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;
+}
`;