From 5dbe9066295f517e8ae34b4f9f7ef819e031f56e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 19:53:02 -0500 Subject: [PATCH] move mouse, hover, click, drag into interact module --- src/kiri/core/init.js | 129 ++++++------------------- src/kiri/core/interact.js | 192 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 101 deletions(-) create mode 100644 src/kiri/core/interact.js diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 6df587d7..efc25321 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -7,6 +7,7 @@ import { local as sdb } from '../../data/local.js'; import { slider } from './slider.js'; import { keyboard } from './keyboard.js'; import { modal } from './modal.js'; +import { interact } from './interact.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; @@ -1114,22 +1115,35 @@ function init_one() { api.platform.update_size(); - function mouseOnHover(int, event, ints) { - if (!api.feature.hover) return; - if (!int) return api.feature.hovers || api.widgets.meshes(); - api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); - } - - function platformOnHover(int, event) { - if (!api.feature.hover) return; - if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); - } - - api.event.on("feature.hover", enable => { - space.mouse.onHover(enable ? mouseOnHover : undefined); - space.platform.onHover(enable ? platformOnHover : undefined); + // initialize interaction controller + interact.init({ + api, + space, + platform, + selection, + ui, + settings, + VIEWS, + onHover: (data) => { + api.event.emit('mouse.hover', data); + }, + onHoverDown: (data) => { + api.event.emit('mouse.hover.down', data); + }, + onHoverUp: (data) => { + api.event.emit('mouse.hover.up', data); + }, + onDragDone: (offset) => { + api.event.emit('mouse.drag.done', offset); + }, + onSelectionDrag: (delta) => { + api.event.emit('selection.drag', delta); + } }); + // expose interact to API + api.interact = interact; + // prevent safari from exiting full screen mode DOC.onkeydown = function (evt) { if (evt.code == 27) evt.preventDefault() @@ -1145,93 +1159,6 @@ function init_one() { } }; - space.mouse.downSelect((int, event) => { - if (api.feature.on_mouse_down) { - if (int) { - api.feature.on_mouse_down(int, event); - return; - } - } - if (api.feature.hover) { - if (int) { - return api.event.emit('mouse.hover.down', {int, point: int.point}); - } else { - return selection.meshes(); - } - } - // lay flat with meta or ctrl clicking a selected face - if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { - let q = new THREE.Quaternion(); - // find intersecting point, look "up" on Z and rotate to face that - q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); - selection.rotate(q); - } - if (api.view.get() !== VIEWS.ARRANGE) { - // return no selection in modes other than arrange - return null; - } else { - // return selected meshes for further mouse processing - return api.feature.hovers || selection.meshes(); - } - }); - - space.mouse.upSelect((object, event) => { - if (api.feature.on_mouse_up) { - if (event && object) { - return api.feature.on_mouse_up(object, event); - } else { - return api.widgets.meshes(); - } - } - if (event && api.feature.hover) { - api.event.emit('mouse.hover.up', { object, event }); - return; - } - if (event && event.target.nodeName === "CANVAS") { - if (object && object.object) { - if (object.object.widget) { - platform.select(object.object.widget, event.shiftKey, false); - } - } else { - platform.deselect(); - } - } else { - return api.feature.hovers || api.widgets.meshes(); - } - }); - - space.mouse.onDrag(function(delta, offset, up = false) { - if (api.feature.hover) { - return; - } - if (up) { - api.event.emit('mouse.drag.done', offset); - } - if (delta && ui.freeLayout.checked) { - let set = settings(); - let dev = set.device; - let bound = set.bounds_sel; - let width = dev.bedWidth/2; - let depth = dev.bedDepth/2; - let isout = ( - bound.min.x <= -width || - bound.min.y <= -depth || - bound.max.x >= width || - bound.max.y >= depth - ); - if (!isout) { - if (bound.min.x + delta.x <= -width) return; - if (bound.min.y + delta.y <= -depth) return; - if (bound.max.x + delta.x >= width) return; - if (bound.max.y + delta.y >= depth) return; - } - selection.move(delta.x, delta.y, 0); - api.event.emit('selection.drag', delta); - } else { - return selection.meshes().length > 0; - } - }); - api.space.restore(init_two) || checkSeed(init_two) || init_two(); }; diff --git a/src/kiri/core/interact.js b/src/kiri/core/interact.js new file mode 100644 index 00000000..59a2348f --- /dev/null +++ b/src/kiri/core/interact.js @@ -0,0 +1,192 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Interaction Controller (Singleton) + * Manages mouse hover, click, and drag interactions with 3D objects + */ + +class InteractionControl { + #api; + #space; + #platform; + #selection; + #ui; + #settings; + #VIEWS; + #initialized = false; + + // Callbacks to replace event emissions + #onHover; + #onHoverDown; + #onHoverUp; + #onDragDone; + #onSelectionDrag; + + /** + * Initialize the interaction controller with dependencies + * @param {Object} config + * @param {Object} config.api - API object + * @param {Object} config.space - Space object (3D scene) + * @param {Object} config.platform - Platform object + * @param {Object} config.selection - Selection object + * @param {Object} config.ui - UI element references + * @param {Function} config.settings - Settings getter function + * @param {Object} config.VIEWS - View constants + * @param {Function} config.onHover - Called when hovering over objects + * @param {Function} config.onHoverDown - Called on mouse down over object + * @param {Function} config.onHoverUp - Called on mouse up over object + * @param {Function} config.onDragDone - Called when drag is complete + * @param {Function} config.onSelectionDrag - Called during selection drag + */ + init({ api, space, platform, selection, ui, settings, VIEWS, onHover, onHoverDown, onHoverUp, onDragDone, onSelectionDrag }) { + if (this.#initialized) { + throw new Error('Interactions already initialized'); + } + + this.#api = api; + this.#space = space; + this.#platform = platform; + this.#selection = selection; + this.#ui = ui; + this.#settings = settings; + this.#VIEWS = VIEWS; + this.#onHover = onHover; + this.#onHoverDown = onHoverDown; + this.#onHoverUp = onHoverUp; + this.#onDragDone = onDragDone; + this.#onSelectionDrag = onSelectionDrag; + + this.#bindHoverHandlers(); + this.#bindMouseHandlers(); + this.#initialized = true; + } + + #bindHoverHandlers() { + // Set up hover handlers + this.#api.event.on("feature.hover", enable => { + this.#space.mouse.onHover(enable ? this.#mouseOnHover.bind(this) : undefined); + this.#space.platform.onHover(enable ? this.#platformOnHover.bind(this) : undefined); + }); + } + + #mouseOnHover(int, event, ints) { + if (!this.#api.feature.hover) return; + if (!int) return this.#api.feature.hovers || this.#api.widgets.meshes(); + this.#onHover?.({int, ints, event, point: int.point, type: 'widget'}); + } + + #platformOnHover(int, event) { + if (!this.#api.feature.hover) return; + if (int) this.#onHover?.({point: int, event, type: 'platform'}); + } + + #bindMouseHandlers() { + // Mouse down handler + this.#space.mouse.downSelect((int, event) => { + // Feature hook for custom mouse down handling + if (this.#api.feature.on_mouse_down) { + if (int) { + this.#api.feature.on_mouse_down(int, event); + return; + } + } + + // Hover mode handling + if (this.#api.feature.hover) { + if (int) { + this.#onHoverDown?.({int, point: int.point}); + return; + } else { + return this.#selection.meshes(); + } + } + + // Lay flat with meta or ctrl clicking a selected face + if (int && (event.ctrlKey || event.metaKey || this.#api.feature.on_face_select)) { + let q = new THREE.Quaternion(); + // find intersecting point, look "up" on Z and rotate to face that + q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); + this.#selection.rotate(q); + } + + if (this.#api.view.get() !== this.#VIEWS.ARRANGE) { + // return no selection in modes other than arrange + return null; + } else { + // return selected meshes for further mouse processing + return this.#api.feature.hovers || this.#selection.meshes(); + } + }); + + // Mouse up handler + this.#space.mouse.upSelect((object, event) => { + // Feature hook for custom mouse up handling + if (this.#api.feature.on_mouse_up) { + if (event && object) { + return this.#api.feature.on_mouse_up(object, event); + } else { + return this.#api.widgets.meshes(); + } + } + + // Hover mode handling + if (event && this.#api.feature.hover) { + this.#onHoverUp?.({ object, event }); + return; + } + + // Regular selection handling + if (event && event.target.nodeName === "CANVAS") { + if (object && object.object) { + if (object.object.widget) { + this.#platform.select(object.object.widget, event.shiftKey, false); + } + } else { + this.#platform.deselect(); + } + } else { + return this.#api.feature.hovers || this.#api.widgets.meshes(); + } + }); + + // Drag handler + this.#space.mouse.onDrag((delta, offset, up = false) => { + if (this.#api.feature.hover) { + return; + } + + if (up) { + this.#onDragDone?.(offset); + } + + if (delta && this.#ui.freeLayout.checked) { + let set = this.#settings(); + let dev = set.device; + let bound = set.bounds_sel; + let width = dev.bedWidth/2; + let depth = dev.bedDepth/2; + let isout = ( + bound.min.x <= -width || + bound.min.y <= -depth || + bound.max.x >= width || + bound.max.y >= depth + ); + + if (!isout) { + if (bound.min.x + delta.x <= -width) return; + if (bound.min.y + delta.y <= -depth) return; + if (bound.max.x + delta.x >= width) return; + if (bound.max.y + delta.y >= depth) return; + } + + this.#selection.move(delta.x, delta.y, 0); + this.#onSelectionDrag?.(delta); + } else { + return this.#selection.meshes().length > 0; + } + }); + } +} + +// Export singleton instance +export const interact = new InteractionControl();