diff --git a/packages/jupytercad-app/src/app/plugins/mainmenu/menuWidget.ts b/packages/jupytercad-app/src/app/plugins/mainmenu/menuWidget.ts index 5a836edc..9b723624 100644 --- a/packages/jupytercad-app/src/app/plugins/mainmenu/menuWidget.ts +++ b/packages/jupytercad-app/src/app/plugins/mainmenu/menuWidget.ts @@ -159,6 +159,10 @@ export class MainMenu extends MenuBar { type: 'command', command: CommandIDs.updateAxes }); + menu.addItem({ + type: 'command', + command: CommandIDs.updateCameraSettings + }); const themeMenu = new RankedMenu({ commands: this._commands, diff --git a/packages/jupytercad-extension/src/commands.ts b/packages/jupytercad-extension/src/commands.ts index 56215189..92ae01f8 100644 --- a/packages/jupytercad-extension/src/commands.ts +++ b/packages/jupytercad-extension/src/commands.ts @@ -382,6 +382,36 @@ const EXPLODED_VIEW_FORM = { } }; +const CAMERA_FORM = { + title: 'Camera Settings', + schema: { + type: 'object', + required: ['Type'], + additionalProperties: false, + properties: { + Type: { + title: 'Projection', + description: 'The projection type', + type: 'string', + enum: ['Perspective', 'Orthographic'] + } + } + }, + default: (panel: JupyterCadPanel) => { + return { + Type: panel.cameraSettings?.type ?? 'Perspective' + }; + }, + syncData: (panel: JupyterCadPanel) => { + return (props: IDict) => { + const { Type } = props; + panel.cameraSettings = { + type: Type + }; + }; + } +}; + /** * Add the FreeCAD commands to the application's command registry. */ @@ -551,6 +581,29 @@ export function addCommands( await dialog.launch(); } }); + + commands.addCommand(CommandIDs.updateCameraSettings, { + label: trans.__('Camera Settings'), + isEnabled: () => Boolean(tracker.currentWidget), + iconClass: 'fa fa-camera', + execute: async () => { + const current = tracker.currentWidget; + + if (!current) { + return; + } + + const dialog = new FormDialog({ + context: current.context, + title: CAMERA_FORM.title, + schema: CAMERA_FORM.schema, + sourceData: CAMERA_FORM.default(current.content), + syncData: CAMERA_FORM.syncData(current.content), + cancelButton: true + }); + await dialog.launch(); + } + }); } /** @@ -575,6 +628,7 @@ export namespace CommandIDs { export const updateAxes = 'jupytercad:updateAxes'; export const updateExplodedView = 'jupytercad:updateExplodedView'; + export const updateCameraSettings = 'jupytercad:updateCameraSettings'; } namespace Private { diff --git a/packages/jupytercad-extension/src/mainview.tsx b/packages/jupytercad-extension/src/mainview.tsx index c1434ea5..98ceedf1 100644 --- a/packages/jupytercad-extension/src/mainview.tsx +++ b/packages/jupytercad-extension/src/mainview.tsx @@ -17,6 +17,7 @@ import { v4 as uuid } from 'uuid'; import { AxeHelper, + CameraSettings, ExplodedView, IAnnotation, IDict, @@ -32,7 +33,6 @@ import { } from './types'; import { FloatingAnnotation } from './annotation/view'; import { getCSSVariableColor, throttle } from './tools'; -import { Vector2 } from 'three'; // Apply the BVH extension THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree; @@ -238,9 +238,9 @@ export class MainView extends React.Component { this._scene.add(new THREE.AmbientLight(0xffffff, 0.5)); // soft white light - const light = new THREE.PointLight(0xffffff, 1); + this._cameraLight = new THREE.PointLight(0xffffff, 1); - this._camera.add(light); + this._camera.add(this._cameraLight); this._scene.add(this._camera); @@ -367,8 +367,15 @@ export class MainView extends React.Component { this.divRef.current.clientHeight, false ); - this._camera.aspect = - this.divRef.current.clientWidth / this.divRef.current.clientHeight; + if (this._camera.type === 'PerspectiveCamera') { + this._camera.aspect = + this.divRef.current.clientWidth / this.divRef.current.clientHeight; + } else { + this._camera.left = this.divRef.current.clientWidth / -2; + this._camera.right = this.divRef.current.clientWidth / 2; + this._camera.top = this.divRef.current.clientHeight / 2; + this._camera.bottom = this.divRef.current.clientHeight / -2; + } this._camera.updateProjectionMatrix(); } }; @@ -406,7 +413,7 @@ export class MainView extends React.Component { copy.project(this._camera); - return new Vector2( + return new THREE.Vector2( (0.5 + copy.x / 2) * canvas.width, (0.5 - copy.y / 2) * canvas.height ); @@ -986,6 +993,16 @@ export class MainView extends React.Component { this._setupExplodedView(); } } + + if (change.key === 'cameraSettings') { + const cameraSettings = change.newValue as CameraSettings | undefined; + + if (change.type !== 'remove' && cameraSettings) { + this._cameraSettings = cameraSettings; + + this._updateCamera(); + } + } } private _setupExplodedView() { @@ -1028,6 +1045,36 @@ export class MainView extends React.Component { } } + private _updateCamera() { + const position = new THREE.Vector3().copy(this._camera.position); + const up = new THREE.Vector3().copy(this._camera.up); + + this._camera.remove(this._cameraLight); + this._scene.remove(this._camera); + + if (this._cameraSettings.type === 'Perspective') { + this._camera = new THREE.PerspectiveCamera(90, 2, 0.1, 1000); + } else { + const width = this.divRef.current?.clientWidth || 0; + const height = this.divRef.current?.clientHeight || 0; + + this._camera = new THREE.OrthographicCamera( + width / -2, + width / 2, + height / 2, + height / -2 + ); + } + + this._camera.add(this._cameraLight); + + this._scene.add(this._camera); + this._controls.object = this._camera; + + this._camera.position.copy(position); + this._camera.up.copy(up); + } + private _computeExplodedState(mesh: BasicMesh) { const center = new THREE.Vector3(); this._boundingGroup.getCenter(center); @@ -1179,9 +1226,11 @@ export class MainView extends React.Component { // TODO Make this a shared property private _explodedView: ExplodedView = { enabled: false, factor: 0 }; private _explodedViewLinesHelperGroup: THREE.Group | null = null; // The list of line helpers for the exploded view + private _cameraSettings: CameraSettings = { type: 'Perspective' }; private _scene: THREE.Scene; // Threejs scene - private _camera: THREE.PerspectiveCamera; // Threejs camera + private _camera: THREE.PerspectiveCamera | THREE.OrthographicCamera; // Threejs camera + private _cameraLight: THREE.PointLight; private _raycaster = new THREE.Raycaster(); private _renderer: THREE.WebGLRenderer; // Threejs render private _requestID: any = null; // ID of window.requestAnimationFrame diff --git a/packages/jupytercad-extension/src/toolbar/widget.tsx b/packages/jupytercad-extension/src/toolbar/widget.tsx index 3d1f4d5a..109fc0ba 100644 --- a/packages/jupytercad-extension/src/toolbar/widget.tsx +++ b/packages/jupytercad-extension/src/toolbar/widget.tsx @@ -164,6 +164,14 @@ export class ToolbarWidget extends Toolbar { commands: options.commands }) ); + this.addItem( + 'Camera Settings', + new CommandToolbarButton({ + id: CommandIDs.updateCameraSettings, + label: '', + commands: options.commands + }) + ); this.addItem('spacer', Toolbar.createSpacerItem()); diff --git a/packages/jupytercad-extension/src/types.ts b/packages/jupytercad-extension/src/types.ts index a85188aa..1925f6ae 100644 --- a/packages/jupytercad-extension/src/types.ts +++ b/packages/jupytercad-extension/src/types.ts @@ -133,6 +133,13 @@ export type ExplodedView = { factor: number; }; +/** + * The state of the camera + */ +export type CameraSettings = { + type: 'Perspective' | 'Orthographic'; +}; + export interface IJcadObjectDocChange { objectChange?: Array<{ name: string; diff --git a/packages/jupytercad-extension/src/widget.tsx b/packages/jupytercad-extension/src/widget.tsx index 28d0ca24..db2f4beb 100644 --- a/packages/jupytercad-extension/src/widget.tsx +++ b/packages/jupytercad-extension/src/widget.tsx @@ -9,6 +9,7 @@ import { MainView } from './mainview'; import { AxeHelper, ExplodedView, + CameraSettings, IJupyterCadModel, IJupyterCadWidget } from './types'; @@ -84,6 +85,14 @@ export class JupyterCadPanel extends ReactWidget { this._view.set('explodedView', value || null); } + get cameraSettings(): CameraSettings | undefined { + return this._view.get('cameraSettings') as CameraSettings | undefined; + } + + set cameraSettings(value: CameraSettings | undefined) { + this._view.set('cameraSettings', value || null); + } + deleteAxes(): void { this._view.delete('axes'); } diff --git a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-example3-FCStd-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-example3-FCStd-linux.png index b1500009..0d3a6d0c 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-example3-FCStd-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-example3-FCStd-linux.png differ diff --git a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-example3-FCStd-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-example3-FCStd-linux.png index d10b6447..b42baa2d 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-example3-FCStd-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-example3-FCStd-linux.png differ