From b59bdee0716504634b24e249cbaf10a772ea38d6 Mon Sep 17 00:00:00 2001 From: Scott Vorthmann Date: Thu, 28 Mar 2024 15:52:31 -0700 Subject: [PATCH] Added snap behavior I'm not happy with the high traffic to the worker, lots of useless snap events due to the poor event API from `TrackballControls` in three.js, but it is working fine for the user. --- online/src/app/59icosahedra/selectors.jsx | 3 +- online/src/app/classic/classic.jsx | 49 +----------- online/src/app/classic/components/camera.jsx | 14 +++- online/src/app/classic/components/editor.jsx | 7 +- .../app/classic/components/strutbuilder.jsx | 2 +- .../src/app/classic/components/toolbars.jsx | 2 +- online/src/app/classic/context/symmetry.jsx | 74 +++++++++++++++++++ online/src/app/classic/index.jsx | 3 +- online/src/app/classic/menus/systemmenu.jsx | 2 +- online/src/app/classic/menus/toolsmenu.jsx | 2 +- online/src/app/classic/tools/snapcamera.jsx | 17 +++++ online/src/app/classic/tools/trackball.jsx | 4 +- online/src/viewer/context/editor.jsx | 1 + online/src/viewer/context/interaction.jsx | 18 ++--- online/src/viewer/ltcanvas.jsx | 4 +- online/src/viewer/trackballcontrols.jsx | 4 + online/src/worker/legacy/controllers/index.js | 18 ++++- .../src/worker/legacy/controllers/wrapper.js | 14 ++-- online/src/worker/vzome-worker-static.js | 7 ++ 19 files changed, 160 insertions(+), 85 deletions(-) create mode 100644 online/src/app/classic/context/symmetry.jsx create mode 100644 online/src/app/classic/tools/snapcamera.jsx diff --git a/online/src/app/59icosahedra/selectors.jsx b/online/src/app/59icosahedra/selectors.jsx index f193750c3..40c70b173 100644 --- a/online/src/app/59icosahedra/selectors.jsx +++ b/online/src/app/59icosahedra/selectors.jsx @@ -35,7 +35,8 @@ const CellSelectorTool = props => onDragStart: ( id, position, type, starting, evt ) => {}, onDrag: evt => {}, onDragEnd: evt => {}, - onContextMenu: ( id, position, type, selected ) => {} + onContextMenu: ( id, position, type, selected ) => {}, + onTrackballEnd: () => {}, }; const [ _, setTool ] = useInteractionTool(); diff --git a/online/src/app/classic/classic.jsx b/online/src/app/classic/classic.jsx index ee037f867..21c00b157 100644 --- a/online/src/app/classic/classic.jsx +++ b/online/src/app/classic/classic.jsx @@ -1,15 +1,10 @@ -import { createSignal, createContext, useContext } from "solid-js"; - -import { controllerProperty, subController, useEditor } from '../../viewer/context/editor.jsx'; +import { subController, useEditor } from '../../viewer/context/editor.jsx'; import { CameraControls } from './components/camera.jsx'; import { StrutBuildPanel } from './components/strutbuilder.jsx'; import { BookmarkBar, ToolBar, ToolFactoryBar } from './components/toolbars.jsx'; import { SceneEditor } from './components/editor.jsx'; -import { OrbitsDialog } from "./dialogs/orbits.jsx"; -import { ShapesDialog } from "./dialogs/shapes.jsx"; -import { PolytopesDialog } from "./dialogs/polytopes.jsx"; import { ErrorAlert } from "./components/alert.jsx"; export const ClassicEditor = () => @@ -51,45 +46,3 @@ export const ClassicEditor = () => ) } - -const SymmetryContext = createContext(); - -export const SymmetryProvider = (props) => -{ - const { rootController, controllerAction } = useEditor(); - const symmetry = () => controllerProperty( rootController(), 'symmetry' ); - const strutBuilder = () => subController( rootController(), 'strutBuilder' ); - const symmController = () => subController( strutBuilder(), `symmetry.${symmetry()}` ); - - const [ showShapesDialog, setShowShapesDialog ] = createSignal( false ); - const [ showOrbitsDialog, setShowOrbitsDialog ] = createSignal( false ); - const [ showPolytopesDialog, setShowPolytopesDialog ] = createSignal( false ); - const api = { - symmetryDefined: () => !!symmetry(), - symmetryController: () => symmController(), - showShapesDialog: () => setShowShapesDialog( true ), - showOrbitsDialog: () => setShowOrbitsDialog( true ), - showPolytopesDialog: () => { - controllerAction( subController( symmController(), 'polytopes' ), 'setQuaternion' ); - setShowPolytopesDialog( true ); - }, - }; - - return ( - - - {props.children} - - - setShowShapesDialog(false) } /> - - setShowOrbitsDialog(false) } /> - - setShowPolytopesDialog(false) } /> - - - - ); -} - -export const useSymmetry = () => useContext( SymmetryContext ); diff --git a/online/src/app/classic/components/camera.jsx b/online/src/app/classic/components/camera.jsx index 66d83b00e..0719f1899 100644 --- a/online/src/app/classic/components/camera.jsx +++ b/online/src/app/classic/components/camera.jsx @@ -8,11 +8,13 @@ import FormControlLabel from "@suid/material/FormControlLabel"; import { useWorkerClient } from '../../../viewer/context/worker.jsx'; import { useEditor } from '../../../viewer/context/editor.jsx'; +import { useSymmetry } from "../context/symmetry.jsx"; import { CameraProvider, useCamera } from '../../../viewer/context/camera.jsx'; -import { CameraTool, InteractionToolProvider } from '../../../viewer/context/interaction.jsx'; - +import { InteractionToolProvider } from '../../../viewer/context/interaction.jsx'; import { SceneCanvas } from '../../../viewer/scenecanvas.jsx'; +import { SnapCameraTool } from '../tools/snapcamera.jsx'; + export const CameraControls = (props) => { @@ -20,6 +22,7 @@ export const CameraControls = (props) => const { isWorkerReady, subscribeFor } = useWorkerClient(); const { rootController, controllerAction } = useEditor(); const { state, setCamera, togglePerspective } = useCamera(); + const { snapping, toggleSnapping } = useSymmetry(); const [ scene, setScene ] = createStore( null ); const isPerspective = () => state.camera.perspective; @@ -78,14 +81,17 @@ export const CameraControls = (props) => {/* provider and CameraTool just to get the desired cursor */} - +
}/> -
snap
+ + }/>
outlines
diff --git a/online/src/app/classic/components/editor.jsx b/online/src/app/classic/components/editor.jsx index 61366aa39..eb25bf9b6 100644 --- a/online/src/app/classic/components/editor.jsx +++ b/online/src/app/classic/components/editor.jsx @@ -9,10 +9,11 @@ import { LabelDialog } from '../dialogs/label.jsx'; import { useCamera } from '../../../viewer/context/camera.jsx'; import { useViewer } from '../../../viewer/context/viewer.jsx'; -import { CameraTool, InteractionToolProvider } from '../../../viewer/context/interaction.jsx'; +import { InteractionToolProvider } from '../../../viewer/context/interaction.jsx'; import { useEditor } from '../../../viewer/context/editor.jsx'; - import { SceneCanvas } from '../../../viewer/index.jsx'; + +import { SnapCameraTool } from '../tools/snapcamera.jsx'; import { SelectionTool } from '../tools/selection.jsx'; import { StrutDragTool } from '../tools/strutdrag.jsx'; import { ContextualMenuArea, resumeMenuKeyEvents, suspendMenuKeyEvents } from '../../framework/menus.jsx'; @@ -109,7 +110,7 @@ export const SceneEditor = ( props ) => }> - + diff --git a/online/src/app/classic/components/strutbuilder.jsx b/online/src/app/classic/components/strutbuilder.jsx index b03757ddc..51dc38e29 100644 --- a/online/src/app/classic/components/strutbuilder.jsx +++ b/online/src/app/classic/components/strutbuilder.jsx @@ -10,7 +10,7 @@ import { controllerProperty, subController, useEditor } from '../../../viewer/co import { StrutLengthPanel } from './length.jsx'; import { OrbitPanel } from './orbitpanel.jsx'; -import { useSymmetry } from "../classic.jsx"; +import { useSymmetry } from "../context/symmetry.jsx"; export const StrutBuildPanel = () => { diff --git a/online/src/app/classic/components/toolbars.jsx b/online/src/app/classic/components/toolbars.jsx index 31ee45095..a137e4876 100644 --- a/online/src/app/classic/components/toolbars.jsx +++ b/online/src/app/classic/components/toolbars.jsx @@ -2,7 +2,7 @@ import { createSignal, createEffect } from "solid-js"; import { controllerProperty, subController, useEditor } from '../../../viewer/context/editor.jsx'; -import { useSymmetry } from "../classic.jsx"; +import { useSymmetry } from "../context/symmetry.jsx"; import { ToolConfig } from "../dialogs/toolconfig.jsx"; const ToolbarSpacer = () => (
) diff --git a/online/src/app/classic/context/symmetry.jsx b/online/src/app/classic/context/symmetry.jsx new file mode 100644 index 000000000..5f3a27a14 --- /dev/null +++ b/online/src/app/classic/context/symmetry.jsx @@ -0,0 +1,74 @@ + +import { createContext, createSignal, useContext } from "solid-js"; + +import { controllerProperty, subController, useEditor } from "../../../viewer/context/editor.jsx"; +import { useCamera } from "../../../viewer/context/camera.jsx"; +import { useWorkerClient } from "../../../viewer/context/worker.jsx"; + +import { ShapesDialog } from "../dialogs/shapes.jsx"; +import { OrbitsDialog } from "../dialogs/orbits.jsx"; +import { PolytopesDialog } from "../dialogs/polytopes.jsx"; +import { unwrap } from "solid-js/store"; + + +const SymmetryContext = createContext(); + +export const SymmetryProvider = (props) => +{ + const { subscribeFor } = useWorkerClient(); + const { state, setCamera } = useCamera(); + const { rootController, controllerAction } = useEditor(); + + const symmetry = () => controllerProperty( rootController(), 'symmetry' ); + const strutBuilder = () => subController( rootController(), 'strutBuilder' ); + const symmController = () => subController( strutBuilder(), `symmetry.${symmetry()}` ); + + const [ showShapesDialog, setShowShapesDialog ] = createSignal( false ); + const [ showOrbitsDialog, setShowOrbitsDialog ] = createSignal( false ); + const [ showPolytopesDialog, setShowPolytopesDialog ] = createSignal( false ); + + const [ snapping, setSnapping ] = createSignal( false ); + const snapCamera = () => + { + if ( snapping() ) { + const { up, lookDir } = state.camera; + controllerAction( symmController(), 'snapCamera', { up: unwrap(up), lookDir: unwrap(lookDir) } ); + } + } + + const api = { + snapping, snapCamera, + toggleSnapping: () => setSnapping( v => !v ), + symmetryDefined: () => !!symmetry(), + symmetryController: () => symmController(), + showShapesDialog: () => setShowShapesDialog( true ), + showOrbitsDialog: () => setShowOrbitsDialog( true ), + showPolytopesDialog: () => { + controllerAction( subController( symmController(), 'polytopes' ), 'setQuaternion' ); + setShowPolytopesDialog( true ); + }, + }; + + subscribeFor( 'CAMERA_SNAPPED', ( data ) => { + const { up, lookDir } = data; + setCamera( data ); + }) + + return ( + + + {props.children} + + + setShowShapesDialog(false) } /> + + setShowOrbitsDialog(false) } /> + + setShowPolytopesDialog(false) } /> + + + + ); +} + +export const useSymmetry = () => useContext( SymmetryContext ); diff --git a/online/src/app/classic/index.jsx b/online/src/app/classic/index.jsx index 7f4b1c9a9..37d12f449 100644 --- a/online/src/app/classic/index.jsx +++ b/online/src/app/classic/index.jsx @@ -17,7 +17,8 @@ import { ViewerProvider } from '../../viewer/context/viewer.jsx'; import { CameraProvider } from '../../viewer/context/camera.jsx'; import { VZomeAppBar } from './components/appbar.jsx'; -import { ClassicEditor, SymmetryProvider } from './classic.jsx'; +import { ClassicEditor } from './classic.jsx'; +import { SymmetryProvider } from './context/symmetry.jsx'; const Persistence = () => { diff --git a/online/src/app/classic/menus/systemmenu.jsx b/online/src/app/classic/menus/systemmenu.jsx index 7f973f177..8b033c255 100644 --- a/online/src/app/classic/menus/systemmenu.jsx +++ b/online/src/app/classic/menus/systemmenu.jsx @@ -2,7 +2,7 @@ import { Choices, Divider, Menu, MenuAction, createCheckboxItem } from "../../framework/menus.jsx"; import { controllerProperty, useEditor } from "../../../viewer/context/editor.jsx"; -import { useSymmetry } from "../classic.jsx"; +import { useSymmetry } from "../context/symmetry.jsx"; export const SystemMenu = () => { diff --git a/online/src/app/classic/menus/toolsmenu.jsx b/online/src/app/classic/menus/toolsmenu.jsx index d9882cfad..19078713b 100644 --- a/online/src/app/classic/menus/toolsmenu.jsx +++ b/online/src/app/classic/menus/toolsmenu.jsx @@ -2,7 +2,7 @@ import { mergeProps } from "solid-js"; import { Divider, Menu, MenuAction, createMenuAction } from "../../framework/menus.jsx"; -import { useSymmetry } from "../classic.jsx"; +import { useSymmetry } from "../context/symmetry.jsx"; import { useEditor } from "../../../viewer/context/editor.jsx"; export const SymmetryAction = ( props ) => diff --git a/online/src/app/classic/tools/snapcamera.jsx b/online/src/app/classic/tools/snapcamera.jsx new file mode 100644 index 000000000..628fcc79a --- /dev/null +++ b/online/src/app/classic/tools/snapcamera.jsx @@ -0,0 +1,17 @@ + +import { onMount } from "solid-js"; + +import { useInteractionTool, grabTool } from "../../../viewer/context/interaction.jsx"; +import { useSymmetry } from "../context/symmetry.jsx"; + + +export const SnapCameraTool = props => +{ + const { snapCamera } = useSymmetry(); + + const [ _, setTool ] = useInteractionTool(); + const cursor = 'url(rotate.svg) 9 9, url(rotate.png) 9 9, grab'; + onMount( () => setTool( { ...grabTool, cursor, onTrackballEnd: snapCamera } ) ); + + return null; +} diff --git a/online/src/app/classic/tools/trackball.jsx b/online/src/app/classic/tools/trackball.jsx index 5b2113e42..652247790 100644 --- a/online/src/app/classic/tools/trackball.jsx +++ b/online/src/app/classic/tools/trackball.jsx @@ -2,8 +2,8 @@ import { Show, createEffect, createSignal, onCleanup } from "solid-js"; import { Matrix4, Quaternion, Vector3 } from 'three'; import { useFrame, useThree } from "solid-three"; -import { DragHandler } from "./drag"; -import { VectorArrow } from "./arrow"; +import { DragHandler } from "./drag.ts"; +import { VectorArrow } from "./arrow.jsx"; const defaultDrag = { direction: [ 1, 0 ], diff --git a/online/src/viewer/context/editor.jsx b/online/src/viewer/context/editor.jsx index 17667fe95..3002a27a5 100644 --- a/online/src/viewer/context/editor.jsx +++ b/online/src/viewer/context/editor.jsx @@ -106,6 +106,7 @@ const EditorProvider = props => case 'FETCH_STARTED': case 'TRACKBALL_SCENE_LOADED': case 'SCENES_DISCOVERED': + case 'CAMERA_SNAPPED': // TODO: do these require any state changes? break; diff --git a/online/src/viewer/context/interaction.jsx b/online/src/viewer/context/interaction.jsx index c10e7b473..bb0f4b26d 100644 --- a/online/src/viewer/context/interaction.jsx +++ b/online/src/viewer/context/interaction.jsx @@ -1,9 +1,9 @@ -import { createContext, createEffect, createSignal, useContext } from "solid-js"; +import { createContext, createSignal, useContext } from "solid-js"; const InteractionToolContext = createContext( [] ); -const grabTool = { +export const grabTool = { allowTrackball: true, // THIS is the reason this component exists @@ -13,7 +13,8 @@ const grabTool = { bkgdClick: () => {}, onDragStart: () => {}, onDrag: evt => {}, - onDragEnd: evt => {} + onDragEnd: evt => {}, + onTrackballEnd: () => {}, }; const InteractionToolProvider = (props) => @@ -29,13 +30,4 @@ const InteractionToolProvider = (props) => const useInteractionTool = () => { return useContext( InteractionToolContext ); }; -const CameraTool = props => -{ - const [ _, setTool ] = useInteractionTool(); - const cursor = 'url(rotate.svg) 9 9, url(rotate.png) 9 9, grab'; - createEffect( () => setTool( { ...grabTool, cursor } ) ); - - return null; -} - -export { InteractionToolProvider, useInteractionTool, CameraTool }; \ No newline at end of file +export { InteractionToolProvider, useInteractionTool }; \ No newline at end of file diff --git a/online/src/viewer/ltcanvas.jsx b/online/src/viewer/ltcanvas.jsx index fc66dec9b..707f18e47 100644 --- a/online/src/viewer/ltcanvas.jsx +++ b/online/src/viewer/ltcanvas.jsx @@ -39,6 +39,8 @@ const LightedCameraControls = (props) => props = mergeProps( { rotateSpeed: 4.5, zoomSpeed: 3, panSpeed: 1 }, props ); const halfWidth = () => perspectiveProps.width / 2; + const onDragEnd = () => ( tool !== undefined ) && tool() .onTrackballEnd(); + return ( <> ); diff --git a/online/src/viewer/trackballcontrols.jsx b/online/src/viewer/trackballcontrols.jsx index 554eb98ce..9c7e558bf 100644 --- a/online/src/viewer/trackballcontrols.jsx +++ b/online/src/viewer/trackballcontrols.jsx @@ -55,6 +55,10 @@ export const TrackballControls = (props) => const onChange = () => props.sync( controls .target, props.name ); controls .addEventListener( "change", onChange ); + const onEnd = () => { + props.trackballEnd && props.trackballEnd( controls .target, props.name ); + } + controls .addEventListener( "end", onEnd ); onCleanup(() => { controls .removeEventListener( "change", onChange ); diff --git a/online/src/worker/legacy/controllers/index.js b/online/src/worker/legacy/controllers/index.js index 1d0d09fc3..b09d0e67c 100644 --- a/online/src/worker/legacy/controllers/index.js +++ b/online/src/worker/legacy/controllers/index.js @@ -81,6 +81,20 @@ const getSceneIndex = ( title, list ) => return index; } +export const snapCamera = ( symmController, upArray, lookArray ) => +{ + const snapper = symmController .getSnapper(); + let up = new com.vzome.core.math.RealVector( ...upArray ); + let look = new com.vzome.core.math.RealVector( ...lookArray ); + look = snapper .snapZ( look ) .normalize(); + up = snapper .snapY( look, up ) .normalize(); + const toArray = rv => { + const { x, y, z } = rv; + return [ x, y, z ]; + } + return { up: toArray( up ), lookDir: toArray( look ) }; +} + export const loadDesign = ( xml, debug, clientEvents, sceneTitle ) => { const design = parse( xml ); @@ -134,6 +148,7 @@ export const loadDesign = ( xml, debug, clientEvents, sceneTitle ) => const embedding = design .getOrbitSource() .getEmbedding(); return { ...renderHistory.getScene(editId, before), embedding }; }; + wrapper.snapCamera = snapCamera; // TODO: fix this terrible hack! wrapper.renderScene = () => renderHistory.recordSnapshot('--END--', '--END--', []); @@ -159,11 +174,10 @@ export const newDesign = ( fieldName, clientEvents ) => const embedding = design .getOrbitSource() .getEmbedding(); return { ...renderHistory.getScene(editId, before), embedding }; }; + wrapper.snapCamera = snapCamera; // TODO: fix this terrible hack! wrapper.renderScene = () => renderHistory.recordSnapshot('--END--', '--END--', []); return wrapper; } - - diff --git a/online/src/worker/legacy/controllers/wrapper.js b/online/src/worker/legacy/controllers/wrapper.js index 3a84bf443..ba884fab2 100644 --- a/online/src/worker/legacy/controllers/wrapper.js +++ b/online/src/worker/legacy/controllers/wrapper.js @@ -49,6 +49,11 @@ export class ControllerWrapper{ } } + getControllerByPath( controllerPath ) { + const controllerNames = controllerPath ? controllerPath.split(':') : []; + return this.getSubControllerByNames(controllerNames); + } + fetchAndFirePropertyChange(propName, isList) { const value = isList ? this.controller.getCommandList(propName) @@ -58,8 +63,7 @@ export class ControllerWrapper{ // This is only ever called on the root controller registerPropertyInterest(controllerPath, propName, changeName, isList) { - const controllerNames = controllerPath ? controllerPath.split(':') : []; - const wrapper = this.getSubControllerByNames(controllerNames); + const wrapper = this.getControllerByPath(controllerPath); if (!wrapper) return; // Happens regularly on startup, when some properties are still undefined @@ -90,8 +94,7 @@ export class ControllerWrapper{ setProperty( controllerPath, name, value ) { - const controllerNames = controllerPath ? controllerPath.split(':') : []; - const wrapper = this.getSubControllerByNames(controllerNames); + const wrapper = this.getControllerByPath(controllerPath); wrapper.controller.setProperty( name, value ); } @@ -106,8 +109,7 @@ export class ControllerWrapper{ // this.macro .push( { controllerPath, action, parameters } ); // } - const controllerNames = controllerPath ? controllerPath.split(':') : []; - const wrapper = this.getSubControllerByNames(controllerNames); + const wrapper = this.getControllerByPath(controllerPath); if (parameters && Object.keys(parameters).length !== 0) wrapper.controller.paramActionPerformed(null, action, new JsProperties(parameters)); diff --git a/online/src/worker/vzome-worker-static.js b/online/src/worker/vzome-worker-static.js index 7d1efd52a..800958ea1 100644 --- a/online/src/worker/vzome-worker-static.js +++ b/online/src/worker/vzome-worker-static.js @@ -452,6 +452,13 @@ onmessage = ({ data }) => connectTrackballScene( postMessage ); return; } + if ( action === 'snapCamera' ) { + const symmController = designWrapper .getControllerByPath( controllerPath ); + const { up, lookDir } = parameters; + const result = designWrapper .snapCamera( symmController.controller, up, lookDir ); + postMessage( { type: 'CAMERA_SNAPPED', payload: { up: result.up, lookDir: result.lookDir } } ); + return; + } try { // console.log( "action", uniqueId ); designWrapper .doAction( controllerPath, action, parameters );