}/>
- 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 );