diff --git a/online/public/test/models/5BrunnVenn.png b/online/public/test/models/5BrunnVenn.png index e86c3c763..4de3ce77b 100644 Binary files a/online/public/test/models/5BrunnVenn.png and b/online/public/test/models/5BrunnVenn.png differ diff --git a/online/src/app/classic/components/camera.jsx b/online/src/app/classic/components/camera.jsx index c1a7882d2..66d83b00e 100644 --- a/online/src/app/classic/components/camera.jsx +++ b/online/src/app/classic/components/camera.jsx @@ -2,6 +2,10 @@ import { createEffect } from 'solid-js'; import { createStore } from 'solid-js/store'; +import Stack from "@suid/material/Stack" +import Switch from "@suid/material/Switch"; +import FormControlLabel from "@suid/material/FormControlLabel"; + import { useWorkerClient } from '../../../viewer/context/worker.jsx'; import { useEditor } from '../../../viewer/context/editor.jsx'; import { CameraProvider, useCamera } from '../../../viewer/context/camera.jsx'; @@ -15,9 +19,11 @@ export const CameraControls = (props) => const context = useCamera(); const { isWorkerReady, subscribeFor } = useWorkerClient(); const { rootController, controllerAction } = useEditor(); - const { state, setCamera } = useCamera(); + const { state, setCamera, togglePerspective } = useCamera(); const [ scene, setScene ] = createStore( null ); + const isPerspective = () => state.camera.perspective; + // TODO: encapsulate these and createStore() in a new createScene()... // OR... // Now that worker and canvas are decoupled, we could just use a separate worker for the trackball scene?! @@ -74,7 +80,15 @@ export const CameraControls = (props) => {/* provider and CameraTool just to get the desired cursor */}
-
perspective | snap | outlines
+ + + }/> +
snap
+
outlines
+
+
diff --git a/online/src/app/classic/components/orbitpanel.jsx b/online/src/app/classic/components/orbitpanel.jsx index 11067efc6..fdd76f884 100644 --- a/online/src/app/classic/components/orbitpanel.jsx +++ b/online/src/app/classic/components/orbitpanel.jsx @@ -1,5 +1,5 @@ -import { Show, For, createEffect, createSignal } from 'solid-js'; +import { Show, For } from 'solid-js'; import Stack from "@suid/material/Stack" import Button from "@suid/material/Button" import Switch from "@suid/material/Switch"; diff --git a/online/src/viewer/cameramode.jsx b/online/src/viewer/cameramode.jsx new file mode 100644 index 000000000..1393e20b3 --- /dev/null +++ b/online/src/viewer/cameramode.jsx @@ -0,0 +1,22 @@ + + +import { Switch } from "@kobalte/core"; + +import { useCamera } from "./context/camera"; + +export const CameraMode = () => +{ + const { togglePerspective, state } = useCamera(); + + return ( +
+ + perspective + + + + + +
+ ); +} diff --git a/online/src/viewer/context/camera.jsx b/online/src/viewer/context/camera.jsx index 2a4b7654f..9aa8f5f4d 100644 --- a/online/src/viewer/context/camera.jsx +++ b/online/src/viewer/context/camera.jsx @@ -122,15 +122,15 @@ const CameraProvider = ( props ) => // thus propagating to all client LightedCameraControls. createEffect( () => { const position = cameraPosition( state.camera ); - const { near, far, up, lookAt } = state.camera; + const { near, far, up, lookAt, width } = state.camera; // I had a nasty bug for days because I used lookAt by reference, causing the CameraControls canvas // to respond very oddly to shared rotations. - setPerspectiveProps( { position, up, target: [ ...lookAt ], near, far } ); + setPerspectiveProps( { position, up, target: [ ...lookAt ], near, far, width } ); }) const trackballCamera = new PerspectiveCamera(); // for the TrackballControls only, never used to render injectCameraState( state.camera, trackballCamera ); - const sync = target => + const sync = ( target, name ) => { // This gets hooked up to TrackballControls changes, and updates the main camera state // from the captive trackballCamera in response. @@ -152,6 +152,7 @@ const CameraProvider = ( props ) => } const setLighting = lighting => setState( 'lighting', lighting ); + const togglePerspective = () => setState( 'camera', 'perspective', val => !val ); const resetCamera = () => { @@ -162,7 +163,7 @@ const CameraProvider = ( props ) => const providerValue = { name: props.name, perspectiveProps, trackballProps, state, - resetCamera, setCamera, setLighting, + resetCamera, setCamera, setLighting, togglePerspective, }; // The perspectiveProps is used to initialize PerspectiveCamera in clients. diff --git a/online/src/viewer/index.jsx b/online/src/viewer/index.jsx index ca717058e..af7d7b437 100644 --- a/online/src/viewer/index.jsx +++ b/online/src/viewer/index.jsx @@ -23,6 +23,7 @@ import { UndoRedoButtons } from './undoredo.jsx'; import { GltfExportProvider } from './geometry.jsx'; import { GltfModel } from './gltf.jsx'; import { VrmlModel } from './vrml.jsx'; +import { CameraMode } from './cameramode.jsx'; let stylesAdded = false; // for the onMount in DesignViewer @@ -104,6 +105,8 @@ const DesignViewer = ( props ) => + + diff --git a/online/src/viewer/ltcanvas.jsx b/online/src/viewer/ltcanvas.jsx index f52e1a573..67b8a8299 100644 --- a/online/src/viewer/ltcanvas.jsx +++ b/online/src/viewer/ltcanvas.jsx @@ -5,6 +5,7 @@ import { createMemo, createRenderEffect, mergeProps, onMount } from "solid-js"; import { createElementSize } from "@solid-primitives/resize-observer"; import { PerspectiveCamera } from "./perspectivecamera.jsx"; +import { OrthographicCamera } from "./orthographiccamera.jsx"; import { TrackballControls } from "./trackballcontrols.jsx"; import { useInteractionTool } from "../viewer/context/interaction.jsx"; import { useCamera } from "../viewer/context/camera.jsx"; @@ -32,19 +33,28 @@ const Lighting = () => const LightedCameraControls = (props) => { - const { perspectiveProps, trackballProps, name } = useCamera(); + const { perspectiveProps, trackballProps, name, state } = useCamera(); const [ tool ] = useInteractionTool(); const enableTrackball = () => ( tool === undefined ) || tool().allowTrackball; props = mergeProps( { rotateSpeed: 4.5, zoomSpeed: 3, panSpeed: 1 }, props ); + const halfWidth = () => perspectiveProps.width / 2; return ( <> - - - - + + + }> + + + + + diff --git a/online/src/viewer/orthographiccamera.jsx b/online/src/viewer/orthographiccamera.jsx new file mode 100644 index 000000000..51a4496fe --- /dev/null +++ b/online/src/viewer/orthographiccamera.jsx @@ -0,0 +1,42 @@ + +// Adapted from https://github.com/nksaraf/react-three-fiber/commit/581d02376d4304fb3bab5445435a61c53cc5cdc2 + +import { createEffect, untrack, onCleanup } from 'solid-js'; +import { useThree } from 'solid-three'; + +export const OrthographicCamera = (props) => +{ + const set = useThree(({ set }) => set); + const scene = useThree(({ scene }) => scene); + const camera = useThree(({ camera }) => camera); + + let cam; + + createEffect(() => { + cam.near = props.near; + cam.far = props.far; + cam.left = -props.halfWidth; + cam.right = props.halfWidth; + const halfHeight = props.halfWidth / props.aspect; + cam.top = halfHeight; + cam.bottom = -halfHeight; + cam.updateProjectionMatrix(); + }); + + createEffect( () => { + const [ x, y, z ] = props.target; + cam .lookAt( x, y, z ); + }); + + createEffect(() => { + const oldCam = untrack(() => camera()); + set()({ camera: cam }); + scene() .add( cam ); // The camera will work without this, but the *lights* won't! + onCleanup(() => { + set()({ camera: oldCam }); + scene() .remove( cam ); + }); + }); + + return +} diff --git a/online/src/viewer/perspectivecamera.jsx b/online/src/viewer/perspectivecamera.jsx index ad82bb850..6afa817a1 100644 --- a/online/src/viewer/perspectivecamera.jsx +++ b/online/src/viewer/perspectivecamera.jsx @@ -7,42 +7,33 @@ import { useThree } from 'solid-three'; export const PerspectiveCamera = (props) => { const set = useThree(({ set }) => set); + const scene = useThree(({ scene }) => scene); const camera = useThree(({ camera }) => camera); const size = useThree(({ size }) => size); - // console.log( 'PerspectiveCamera sees', props.name ); let cam; createEffect( () => { - // console.log( props.name, 'PerspectiveCamera lookAt' ); const [ x, y, z ] = props.target; cam .lookAt( x, y, z ); - - // { - // console.log( props.name, 'look at', JSON.stringify( props.target ) ); - // console.log( props.name, 'position', JSON.stringify( cam.position ) ); - // } }); createEffect(() => { - // console.log( props.name, 'PerspectiveCamera updateProjectionMatrix' ); cam.near = props.near; cam.far = props.far; cam.fov = props.fov; cam.aspect = size().width / size().height; cam.updateProjectionMatrix(); - - // { - // const { near, far, fov, aspect } = cam; - // console.log( props.name, 'zoom', JSON.stringify( { near, far, fov, aspect }, null, 2 ) ); - // console.log( props.name, 'position', JSON.stringify( cam.position ) ); - // } }); createEffect(() => { const oldCam = untrack(() => camera()); set()({ camera: cam }); - onCleanup(() => set()({ camera: oldCam })); + scene() .add( cam ); // The camera will work without this, but the *lights* won't! + onCleanup(() => { + set()({ camera: oldCam }); + scene() .remove( cam ); + }); }); return diff --git a/online/src/viewer/trackballcontrols.jsx b/online/src/viewer/trackballcontrols.jsx index 303628e3e..554eb98ce 100644 --- a/online/src/viewer/trackballcontrols.jsx +++ b/online/src/viewer/trackballcontrols.jsx @@ -53,7 +53,7 @@ export const TrackballControls = (props) => controls.connect( gl().domElement ); - const onChange = () => props.sync( trackballControls() .target ); + const onChange = () => props.sync( controls .target, props.name ); controls .addEventListener( "change", onChange ); onCleanup(() => { diff --git a/online/src/viewer/urlviewer.css.js b/online/src/viewer/urlviewer.css.js index ac308a654..6e789efdf 100644 --- a/online/src/viewer/urlviewer.css.js +++ b/online/src/viewer/urlviewer.css.js @@ -455,4 +455,44 @@ export const urlViewerCSS = ` opacity: 0; } } + +.switch { + display: inline-flex; + align-items: center; +} +.switch__control { + display: inline-flex; + align-items: center; + height: 19px; + width: 33px; + border: 1px solid hsl(240 5% 84%); + border-radius: 12px; + padding: 0 2px; + background-color: hsl(240 6% 90%); + transition: 250ms background-color; +} +.switch__input:focus-visible + .switch__control { + outline: 2px solid hsl(200 98% 39%); + outline-offset: 2px; +} +.switch__control[data-checked] { + border-color: hsl(200 98% 39%); + background-color: hsl(200 98% 39%); +} +.switch__thumb { + height: 17px; + width: 17px; + border-radius: 10px; + background-color: white; + transition: 250ms transform; +} +.switch__thumb[data-checked] { + transform: translateX(calc(100% - 1px)); +} +.switch__label { + margin-right: 6px; + color: hsl(240 6% 10%); + font-size: 14px; + user-select: none; +} `;