Skip to content

Commit

Permalink
Merge pull request #826 from vorth/orthographic
Browse files Browse the repository at this point in the history
Added orthographic camera
  • Loading branch information
vorth authored Mar 15, 2024
2 parents d7ab7a4 + ad49d50 commit 6af9af4
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 30 deletions.
Binary file modified online/public/test/models/5BrunnVenn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions online/src/app/classic/components/camera.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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?!
Expand Down Expand Up @@ -74,7 +80,15 @@ export const CameraControls = (props) =>
{/* provider and CameraTool just to get the desired cursor */}
<CameraTool/>
<div id='camera-controls' style={{ display: 'grid', 'grid-template-rows': 'min-content min-content' }}>
<div id='camera-buttons' class='placeholder' style={{ 'min-height': '60px' }} >perspective | snap | outlines</div>
<Stack spacing={2} direction="row" style={{ padding: '8px' }}>
<FormControlLabel label="perspective"
control={
<Switch checked={isPerspective()} onChange={togglePerspective} size='small' inputProps={{ "aria-label": "controlled" }} />
}/>
<div id='snap-switch' class='placeholder' >snap</div>
<div id='outlines-switch' class='placeholder' >outlines</div>
</Stack>

<div id="ball-and-slider" style={{ display: 'grid', 'grid-template-columns': 'min-content 1fr' }}>
<div id="camera-trackball" style={{ border: '1px solid' }}>
<SceneCanvas scene={scene} height="200px" width="240px" rotationOnly={true} rotateSpeed={0.7}/>
Expand Down
2 changes: 1 addition & 1 deletion online/src/app/classic/components/orbitpanel.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
22 changes: 22 additions & 0 deletions online/src/viewer/cameramode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@


import { Switch } from "@kobalte/core";

import { useCamera } from "./context/camera";

export const CameraMode = () =>
{
const { togglePerspective, state } = useCamera();

return (
<div style={ { position: 'absolute', top: '1em', right: '1em' } }>
<Switch.Root checked={state.camera.perspective} onChange={togglePerspective}>
<Switch.Label class="switch__label">perspective</Switch.Label>
<Switch.Input class="switch__input" />
<Switch.Control class="switch__control">
<Switch.Thumb class="switch__thumb" />
</Switch.Control>
</Switch.Root>
</div>
);
}
9 changes: 5 additions & 4 deletions online/src/viewer/context/camera.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -152,6 +152,7 @@ const CameraProvider = ( props ) =>
}

const setLighting = lighting => setState( 'lighting', lighting );
const togglePerspective = () => setState( 'camera', 'perspective', val => !val );

const resetCamera = () =>
{
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions online/src/viewer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -104,6 +105,8 @@ const DesignViewer = ( props ) =>
<UndoRedoButtons root={rootRef} />
</Show>

<CameraMode/>

<Show when={showSceneMenu()}>
<SceneMenu root={rootRef} show={whichScenes()} />
</Show>
Expand Down
24 changes: 17 additions & 7 deletions online/src/viewer/ltcanvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<>
<PerspectiveCamera aspect={props.aspect} name={name}
position={perspectiveProps.position} up={perspectiveProps.up} fov={perspectiveProps.fov( props.aspect )}
near={perspectiveProps.near} far={perspectiveProps.far} target={perspectiveProps.target} >
<Lighting/>
</PerspectiveCamera>
<TrackballControls enabled={enableTrackball()} rotationOnly={props.rotationOnly}
<Show when={state.camera.perspective} fallback={
<OrthographicCamera aspect={props.aspect} name={name}
position={perspectiveProps.position} up={perspectiveProps.up} halfWidth={halfWidth()}
near={perspectiveProps.near} far={perspectiveProps.far} target={perspectiveProps.target} >
<Lighting />
</OrthographicCamera>
}>
<PerspectiveCamera aspect={props.aspect} name={name}
position={perspectiveProps.position} up={perspectiveProps.up} fov={perspectiveProps.fov( props.aspect )}
near={perspectiveProps.near} far={perspectiveProps.far} target={perspectiveProps.target} >
<Lighting />
</PerspectiveCamera>
</Show>
<TrackballControls enabled={enableTrackball()} rotationOnly={props.rotationOnly} name={name}
camera={trackballProps.camera} target={perspectiveProps.target} sync={trackballProps.sync}
rotateSpeed={props.rotateSpeed} zoomSpeed={props.zoomSpeed} panSpeed={props.panSpeed} />
</>
Expand Down
42 changes: 42 additions & 0 deletions online/src/viewer/orthographiccamera.jsx
Original file line number Diff line number Diff line change
@@ -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 <orthographicCamera ref={cam} position={props.position} {...props} />
}
21 changes: 6 additions & 15 deletions online/src/viewer/perspectivecamera.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <perspectiveCamera ref={cam} position={props.position} {...props} />
Expand Down
2 changes: 1 addition & 1 deletion online/src/viewer/trackballcontrols.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
40 changes: 40 additions & 0 deletions online/src/viewer/urlviewer.css.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
`;

0 comments on commit 6af9af4

Please sign in to comment.