diff --git a/package.json b/package.json index 92a73670..eb42dd7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vim-web", - "version": "0.5.0-dev.17", + "version": "0.5.0-dev.24", "description": "A demonstration app built on top of the vim-webgl-viewer", "type": "module", "files": [ diff --git a/src/vim-web/core-viewers/shared/mouseHandler.ts b/src/vim-web/core-viewers/shared/mouseHandler.ts index 4d1de34c..63145587 100644 --- a/src/vim-web/core-viewers/shared/mouseHandler.ts +++ b/src/vim-web/core-viewers/shared/mouseHandler.ts @@ -11,6 +11,7 @@ export class MouseHandler extends BaseInputHandler { private _capture: CaptureHandler; private _dragHandler: DragHandler; private _doubleClickHandler: DoubleClickHandler = new DoubleClickHandler(); + private _clickHandler: ClickHandler = new ClickHandler(); onButtonDown: (pos: THREE.Vector2, button: number) => void; onButtonUp: (pos: THREE.Vector2, button: number) => void; @@ -47,27 +48,36 @@ export class MouseHandler extends BaseInputHandler { this._lastMouseDownPosition = pos; // Start drag this._dragHandler.onPointerDown(pos, event.button); + this._clickHandler.onPointerDown(pos); this._capture.onPointerDown(event); event.preventDefault(); } private handlePointerUp(event: PointerEvent): void { if (event.pointerType !== 'mouse') return; + event.preventDefault(); + const pos = this.relativePosition(event); // Button up event this.onButtonUp?.(pos, event.button); this._capture.onPointerUp(event); this._dragHandler.onPointerUp(); + this._clickHandler.onPointerUp(); + // Click type event - if(this._doubleClickHandler.checkForDoubleClick(event)){ + if(this._doubleClickHandler.isDoubleClick(event)){ this.handleDoubleClick(event); - }else{ + return + } + if(this._clickHandler.isClick(event)){ this.handleMouseClick(event); - this.handleContextMenu(event); + return } - event.preventDefault(); + + this.handleContextMenu(event); + } private async handleMouseClick(event: PointerEvent): Promise { @@ -102,8 +112,8 @@ export class MouseHandler extends BaseInputHandler { if (event.pointerType !== 'mouse') return; this._canvas.focus(); const pos = this.relativePosition(event); - this._dragHandler.onPointerMove(pos); + this._clickHandler.onPointerMove(pos); this.onMouseMove?.(pos); } @@ -157,13 +167,36 @@ class CaptureHandler { } } +class ClickHandler { + private _moved: boolean = false; + private _startPosition: THREE.Vector2 = new THREE.Vector2(); + private _clickThreshold: number = 0.003 ; + + onPointerDown(pos: THREE.Vector2): void { + this._moved = false; + this._startPosition.copy(pos); + } + onPointerMove(pos: THREE.Vector2): void { + if (pos.distanceTo(this._startPosition) > this._clickThreshold) { + this._moved = true; + } + } + + onPointerUp(): void { } + + isClick(event: PointerEvent): boolean { + if (event.button !== 0) return false; // Only left button + return !this._moved; + } +} + class DoubleClickHandler { private _lastClickTime: number = 0; private _clickDelay: number = 300; // Max time between clicks for double-click private _lastClickPosition: THREE.Vector2 | null = null; private _positionThreshold: number = 5; // Max pixel distance between clicks - checkForDoubleClick(event: MouseEvent): boolean { + isDoubleClick(event: MouseEvent): boolean { const currentTime = Date.now(); const currentPosition = new THREE.Vector2(event.clientX, event.clientY); const timeDiff = currentTime - this._lastClickTime; diff --git a/src/vim-web/core-viewers/ultra/viewport.ts b/src/vim-web/core-viewers/ultra/viewport.ts index 93cd4266..275e5501 100644 --- a/src/vim-web/core-viewers/ultra/viewport.ts +++ b/src/vim-web/core-viewers/ultra/viewport.ts @@ -8,8 +8,12 @@ import { RpcSafeClient } from "./rpcSafeClient"; export interface IViewport { /** The HTML canvas element used for rendering */ canvas: HTMLCanvasElement + /** Updates the aspect ratio of the viewport on the server */ update(): void + + /** Resizes the viewport to match its parent's dimensions */ + resizeToParent(): void } /** @@ -57,6 +61,13 @@ export class Viewport { } } + /** + * Resizes the viewport to match its parent's dimensions + */ + resizeToParent() { + this.update() + } + /** * Cleans up resources by removing resize observer and clearing timeouts */ diff --git a/src/vim-web/core-viewers/webgl/loader/mesh.ts b/src/vim-web/core-viewers/webgl/loader/mesh.ts index 00067fdc..667dc580 100644 --- a/src/vim-web/core-viewers/webgl/loader/mesh.ts +++ b/src/vim-web/core-viewers/webgl/loader/mesh.ts @@ -94,20 +94,49 @@ export class Mesh { } /** - * Overrides mesh material, set to undefine to restore initial material. - */ - setMaterial (value: ModelMaterial) { - if (this._material === value) return - if (this.ignoreSceneMaterial) return - this.mesh.material = value ?? this._material - - // Update material groups - this.mesh.geometry.clearGroups() - if(value instanceof Array) { - value.forEach((m, i) => { - this.mesh.geometry.addGroup(0, Infinity, i) - }) + * Sets the material for this mesh. + * Set to undefined to reset to original materials. + */ + setMaterial(value: ModelMaterial) { + if (this.ignoreSceneMaterial) return; + + const base = this._material; // always defined + let mat: ModelMaterial; + + if (Array.isArray(value)) { + mat = this._mergeMaterials(value, base); + } else { + mat = value ?? base; } + + // Apply it + this.mesh.material = mat; + + // Update groups + this.mesh.geometry.clearGroups(); + if (Array.isArray(mat)) { + mat.forEach((_m, i) => { + this.mesh.geometry.addGroup(0, Infinity, i); + }); + } + } + + private _mergeMaterials( + value: THREE.Material[], + base: ModelMaterial + ): THREE.Material[] { + const baseArr = Array.isArray(base) ? base : [base]; + const result: THREE.Material[] = []; + + for (const v of value) { + if (v === undefined) { + result.push(...baseArr); + } else { + result.push(v); + } + } + + return result; } /** diff --git a/src/vim-web/core-viewers/webgl/loader/progressive/insertableMesh.ts b/src/vim-web/core-viewers/webgl/loader/progressive/insertableMesh.ts index 811d37cb..d2bdd5c4 100644 --- a/src/vim-web/core-viewers/webgl/loader/progressive/insertableMesh.ts +++ b/src/vim-web/core-viewers/webgl/loader/progressive/insertableMesh.ts @@ -129,20 +129,49 @@ export class InsertableMesh { // } } - /** - * Overrides mesh material, set to undefine to restore initial material. - */ - setMaterial (value: ModelMaterial) { - if (this._material === value) return - if (this.ignoreSceneMaterial) return - this.mesh.material = value ?? this._material - - // Update material groups - this.mesh.geometry.clearGroups() - if(value instanceof Array) { - value.forEach((m, i) => { - this.mesh.geometry.addGroup(0, Infinity, i) - }) - } - } + /** + * Sets the material for this mesh. + * Set to undefined to reset to original materials. + */ + setMaterial(value: ModelMaterial) { + if (this.ignoreSceneMaterial) return; + + const base = this._material; // always defined + let mat: ModelMaterial; + + if (Array.isArray(value)) { + mat = this._mergeMaterials(value, base); + } else { + mat = value ?? base; + } + + // Apply it + this.mesh.material = mat; + + // Update groups + this.mesh.geometry.clearGroups(); + if (Array.isArray(mat)) { + mat.forEach((_m, i) => { + this.mesh.geometry.addGroup(0, Infinity, i); + }); + } + } + + private _mergeMaterials( + value: THREE.Material[], + base: ModelMaterial + ): THREE.Material[] { + const baseArr = Array.isArray(base) ? base : [base]; + const result: THREE.Material[] = []; + + for (const v of value) { + if (v === undefined) { + result.push(...baseArr); + } else { + result.push(v); + } + } + + return result; + } } diff --git a/src/vim-web/core-viewers/webgl/loader/progressive/instancedMesh.ts b/src/vim-web/core-viewers/webgl/loader/progressive/instancedMesh.ts index 5eb8e036..504cbc49 100644 --- a/src/vim-web/core-viewers/webgl/loader/progressive/instancedMesh.ts +++ b/src/vim-web/core-viewers/webgl/loader/progressive/instancedMesh.ts @@ -70,19 +70,51 @@ export class InstancedMesh { return submeshes } - setMaterial (value: ModelMaterial) { - if (this._material === value) return - if (this.ignoreSceneMaterial) return - this.mesh.material = value ?? this._material - - // Update material groups - this.mesh.geometry.clearGroups() - if(value instanceof Array) { - value.forEach((m, i) => { - this.mesh.geometry.addGroup(0, Infinity, i) - }) + /** + * Sets the material for this mesh. + * Set to undefined to reset to original materials. + */ + setMaterial(value: ModelMaterial) { + if (this.ignoreSceneMaterial) return; + + const base = this._material; // always defined + let mat: ModelMaterial; + + if (Array.isArray(value)) { + mat = this._mergeMaterials(value, base); + } else { + mat = value ?? base; + } + + // Apply it + this.mesh.material = mat; + + // Update groups + this.mesh.geometry.clearGroups(); + if (Array.isArray(mat)) { + mat.forEach((_m, i) => { + this.mesh.geometry.addGroup(0, Infinity, i); + }); + } + } + + private _mergeMaterials( + value: THREE.Material[], + base: ModelMaterial + ): THREE.Material[] { + const baseArr = Array.isArray(base) ? base : [base]; + const result: THREE.Material[] = []; + + for (const v of value) { + if (v === undefined) { + result.push(...baseArr); + } else { + result.push(v); + } + } + + return result; } - } private computeBoundingBoxes () { this.mesh.geometry.computeBoundingBox() diff --git a/src/vim-web/core-viewers/webgl/viewer/gizmos/markers/gizmoMarker.ts b/src/vim-web/core-viewers/webgl/viewer/gizmos/markers/gizmoMarker.ts index 009d9f76..43d64b89 100644 --- a/src/vim-web/core-viewers/webgl/viewer/gizmos/markers/gizmoMarker.ts +++ b/src/vim-web/core-viewers/webgl/viewer/gizmos/markers/gizmoMarker.ts @@ -43,6 +43,10 @@ export class Marker implements IVimElement { return this._submesh.index } + get isRoom(): boolean { + return false + } + private _outlineAttribute: WebglAttribute private _visibleAttribute: WebglAttribute private _coloredAttribute: WebglAttribute diff --git a/src/vim-web/core-viewers/webgl/viewer/rendering/renderer.ts b/src/vim-web/core-viewers/webgl/viewer/rendering/renderer.ts index 7a529686..edb2d77c 100644 --- a/src/vim-web/core-viewers/webgl/viewer/rendering/renderer.ts +++ b/src/vim-web/core-viewers/webgl/viewer/rendering/renderer.ts @@ -146,14 +146,22 @@ export class Renderer implements IRenderer { this.needsUpdate = true } + /** + * Sets the material used to render models. If set to undefined, the default model or mesh material is used. + */ get modelMaterial () { return this._scene.modelMaterial } set modelMaterial (material: ModelMaterial) { - this._scene.modelMaterial = material + this._scene.modelMaterial = material ?? this.defaultModelMaterial } + /** + * The material that will be used when setting model material to undefined. + */ + defaultModelMaterial: ModelMaterial + /** * Signal dispatched at the end of each frame if the scene was updated, such as visibility changes. */ diff --git a/src/vim-web/core-viewers/webgl/viewer/viewport.ts b/src/vim-web/core-viewers/webgl/viewer/viewport.ts index 0bea9fea..84191812 100644 --- a/src/vim-web/core-viewers/webgl/viewer/viewport.ts +++ b/src/vim-web/core-viewers/webgl/viewer/viewport.ts @@ -150,7 +150,7 @@ export class Viewport { /** * Resizes the canvas and updates the camera to match new parent dimensions. */ - ResizeToParent () { + resizeToParent () { this._onResize.dispatch() } diff --git a/src/vim-web/react-viewers/bim/bimPanel.tsx b/src/vim-web/react-viewers/bim/bimPanel.tsx index fc239424..f675b835 100644 --- a/src/vim-web/react-viewers/bim/bimPanel.tsx +++ b/src/vim-web/react-viewers/bim/bimPanel.tsx @@ -5,9 +5,7 @@ import React, { useMemo, useState } from 'react' import * as Core from '../../core-viewers' -import { AugmentedElement } from '../helpers/element' import { whenAllTrue, whenFalse, whenSomeTrue, whenTrue } from '../helpers/utils' -import { Settings, isFalse } from '../settings' import { CameraRef } from '../state/cameraState' import { IsolationRef } from '../state/sharedIsolation' import { ViewerState } from '../webgl/viewerState' @@ -16,6 +14,8 @@ import { BimInfoPanel } from './bimInfoPanel' import { BimSearch } from './bimSearch' import { BimTree, TreeActionRef } from './bimTree' import { toTreeData } from './bimTreeData' +import { WebglSettings } from '../webgl/settings' +import { isFalse } from '../settings/userBoolean' // Not sure why I need this, // when I inline this method in viewer.tsx it causes an error. @@ -26,13 +26,13 @@ export function OptionalBimPanel (props: { viewerState: ViewerState isolation: IsolationRef visible: boolean - settings: Settings + settings: WebglSettings treeRef: React.MutableRefObject bimInfoRef: BimInfoPanelRef }) { return whenSomeTrue([ - props.settings.ui.bimTreePanel, - props.settings.ui.bimInfoPanel], + props.settings.ui.panelBimTree, + props.settings.ui.panelBimInfo], React.createElement(BimPanel, props)) } @@ -51,7 +51,7 @@ export function BimPanel (props: { viewerState: ViewerState isolation: IsolationRef visible: boolean - settings: Settings + settings: WebglSettings treeRef: React.MutableRefObject bimInfoRef: BimInfoPanelRef }) { @@ -63,11 +63,11 @@ export function BimPanel (props: { const selection = props.viewerState.selection.get() const last = selection[selection.length - 1] - const fullTree = isFalse(props.settings.ui.bimInfoPanel) - const fullInfo = isFalse(props.settings.ui.bimTreePanel) + const fullTree = isFalse(props.settings.ui.panelBimInfo) + const fullInfo = isFalse(props.settings.ui.panelBimTree) return (
- {whenTrue(props.settings.ui.bimTreePanel, + {whenTrue(props.settings.ui.panelBimTree,
{

@@ -92,19 +92,19 @@ export function BimPanel (props: { { // Divider if needed. whenAllTrue([ - props.settings.ui.bimTreePanel, - props.settings.ui.bimInfoPanel, + props.settings.ui.panelBimTree, + props.settings.ui.panelBimInfo, props.viewerState.elements.get()?.length > 0, ], divider()) } - {whenTrue(props.settings.ui.bimInfoPanel, + {whenTrue(props.settings.ui.panelBimInfo,
)} diff --git a/src/vim-web/react-viewers/controlbar/controlBarIds.ts b/src/vim-web/react-viewers/controlbar/controlBarIds.ts index d505ab1a..99d1aed9 100644 --- a/src/vim-web/react-viewers/controlbar/controlBarIds.ts +++ b/src/vim-web/react-viewers/controlbar/controlBarIds.ts @@ -1,52 +1,43 @@ -// Sections -export const sectionSelection = 'controlBar.sectionSelection' -export const sectionCamera = 'controlBar.sectionCamera' -export const sectionInputs = 'controlBar.sectionInputs' -export const sectionActions = 'controlBar.sectionActions' -export const sectionTools = 'controlBar.sectionTools' -export const sectionSettings = 'controlBar.sectionSettings' -export const sectionMeasure = 'controlBar.sectionMeasure' -export const sectionSectionBox = 'controlBar.sectionSectionBox' - // Camera buttons -export const buttonCameraFrameSelection = 'controlBar.camera.frameSelection' -export const buttonCameraFrameScene = 'controlBar.camera.frameScene' -export const buttonCameraAuto = 'controlBar.camera.auto' +export const cameraSpan = 'controlBar.cameraSpan' +export const cameraFrameSelection = 'controlBar.cameraFrameSelection' +export const cameraFrameScene = 'controlBar.cameraFrameScene' +export const cameraAuto = 'controlBar.cameraAuto' // Camera Control buttons -export const buttonCameraOrbit = 'controlBar.camera.orbit' -export const buttonCameraLook = 'controlBarcamera.look' -export const buttonCameraPan = 'controlBar.camera.pan' -export const buttonCameraZoom = 'controlBar.camera.zoom' -export const buttonCameraZoomWindow = 'controlBar.camera.zoomWindow' +export const cursorSpan = 'controlBar.cursorSpan' +export const cursorOrbit = 'controlBar.cursorOrbit' +export const cursorLook = 'controlBar.cursorLook' +export const cursorPan = 'controlBar.cursorPan' +export const cursorZoom = 'controlBar.cursorZoom' +export const cursorZoomWindow = 'controlBar.cursorZoomWindow' + +// Visibility buttons +export const visibilitySpan = 'controlBar.visibilitySpan' +export const visibilityClearSelection = 'controlBar.visibilityClearSelection' +export const visibilityShowAll = 'controlBar.visibilityShowAll' +export const visibilityIsolateSelection = 'controlBar.visibilityIsolateSelection' +export const visibilityHideSelection = 'controlBar.visibilityHideSelection' +export const visibilityShowSelection = 'controlBar.visibilityShowSelection' +export const visibilityAutoIsolate = 'controlBar.visibilityAutoIsolate' +export const visibilitySettings = 'controlBar.visibilitySettings' + +// Section buttons +export const sectioningSpan = 'controlBar.sectioningSpan' +export const sectioningEnable = 'controlBar.sectioningEnable' +export const sectioningVisible = 'controlBar.sectioningVisible' +export const sectioningFitSelection = 'controlBar.sectioningFitSelection' +export const sectioningFitScene = 'controlBar.sectioningFitScene' +export const sectioningAuto = 'controlBar.sectioningAuto' +export const sectioningSettings = 'controlBar.sectioningSettings' + +// Measure buttons +export const measureSpan = 'controlBar.measureSpan' +export const measureEnable = 'controlBar.measureEnable' // Settings buttons -export const buttonProjectInspector = 'controlBar.projectInspector' -export const buttonSettings = 'controlBar.settings' -export const buttonHelp = 'controlBar.help' -export const buttonMaximize = 'controlBar.maximize' - -// Selection buttons -export const buttonClearSelection = 'controlBar.action.clearSelection' -export const buttonShowAll = 'controlBar.selection.showAll' -export const buttonIsolateSelection = 'controlBar.selection.isolate' -export const buttonHideSelection = 'controlBar.selection.hide' -export const buttonShowSelection = 'controlBar.selection.show' -export const buttonAutoIsolate = 'controlBar.selection.autoIsolate' -export const buttonIsolationSettings = 'controlBar.selection.isolationSettings' - -// Action Buttons -export const buttonRenderSettings = 'controlBar.action.renderSettings' - -// Tools buttons -export const buttonSectionBox = 'controlBar.sectionBox' -export const buttonMeasure = 'controlBar.measure' - -// Section box buttons -export const buttonSectionBoxEnable = 'controlBar.sectionBox.enable' -export const buttonSectionBoxVisible = 'controlBar.sectionBox.visible' - -export const buttonSectionBoxToSelection = 'controlBar.sectionBox.sectionSelection' -export const buttonSectionBoxToScene = 'controlBar.sectionBox.sectionScene' -export const buttonSectionBoxAuto = 'controlBar.sectionBox.auto' -export const buttonSectionBoxSettings = 'controlBar.sectionBox.settings' +export const miscSpan = 'controlBar.miscSpan' +export const miscInspector = 'controlBar.miscInspector' +export const miscSettings = 'controlBar.miscSettings' +export const miscHelp = 'controlBar.miscHelp' +export const miscMaximize = 'controlBar.miscMaximize' diff --git a/src/vim-web/react-viewers/errors/errorStyle.tsx b/src/vim-web/react-viewers/errors/errorStyle.tsx index 9db7f117..8ac9620f 100644 --- a/src/vim-web/react-viewers/errors/errorStyle.tsx +++ b/src/vim-web/react-viewers/errors/errorStyle.tsx @@ -5,13 +5,8 @@ export const vcLink = `${vcColorLink} vc-underline` export const vcLabel = 'vc-text-[#3F444F]' export const vcRoboto = 'vc-font-[\'Roboto\',sans-serif]' -export function footer (url: string) { - return ( -

- More troubleshooting tips can be found{' '} - {link(url, 'here')} -

- ) +export function footer () { + return <> } export function mainText (text: JSX.Element) { diff --git a/src/vim-web/react-viewers/helpers/reactUtils.ts b/src/vim-web/react-viewers/helpers/reactUtils.ts index 0796e856..24c605ff 100644 --- a/src/vim-web/react-viewers/helpers/reactUtils.ts +++ b/src/vim-web/react-viewers/helpers/reactUtils.ts @@ -89,19 +89,27 @@ export function useRefresher() : StateRefresher{ * The reference provides access to the state, along with event dispatching, validation, and confirmation logic. * * @param initialValue - The initial state value. + * @param isLazy - Whether to treat the initialValue as a lazy initializer function. * @returns An object implementing StateRef along with additional helper hooks. */ -export function useStateRef(initialValue: T | (() => T)) { - const [value, setValue] = useState(initialValue); - - // https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents - const ref = useRef(undefined); - if(ref.current === undefined) { - if (typeof initialValue === "function") { - ref.current = (initialValue as () => T)(); - } else { - ref.current = initialValue; + +export function useStateRef(initialValue: T | (() => T), isLazy = false) { + const getInitialValue = (): T => { + if (isLazy && typeof initialValue === 'function') { + return (initialValue as () => T)(); } + return initialValue as T; + }; + + // Box the state value to prevent React from ever calling it + type Box = { current: T }; + const [box, setBox] = useState>(() => ({ + current: getInitialValue() + })); + + const ref = useRef(undefined!); + if (ref.current === undefined) { + ref.current = getInitialValue(); } const event = useRef(new SimpleEventDispatcher()); @@ -119,7 +127,7 @@ export function useStateRef(initialValue: T | (() => T)) { if (finalValue === ref.current) return; ref.current = finalValue; - setValue(finalValue); + setBox({ current: finalValue }); event.current.dispatch(finalValue); }; @@ -164,7 +172,7 @@ export function useStateRef(initialValue: T | (() => T)) { * @returns The memoized value. */ useMemo(on: (value: T) => TOut, deps?: any[]) { - return useMemo(() => on(value), [...(deps || []), value]); + return useMemo(() => on(box.current), [...(deps || []), box.current]); }, /** diff --git a/src/vim-web/react-viewers/helpers/utils.ts b/src/vim-web/react-viewers/helpers/utils.ts index a5fbfb31..1f13eb00 100644 --- a/src/vim-web/react-viewers/helpers/utils.ts +++ b/src/vim-web/react-viewers/helpers/utils.ts @@ -24,3 +24,16 @@ export function whenSomeFalse (value: (UserBoolean| boolean)[], element: JSX.Ele return value.some(isFalse) ? element : null } + +/** + * Makes all fields optional recursively + * @template T - The type to make recursively partial + * @returns A type with all nested properties made optional + */ +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object + ? RecursivePartial + : T[P] +} \ No newline at end of file diff --git a/src/vim-web/react-viewers/panels/axesPanel.tsx b/src/vim-web/react-viewers/panels/axesPanel.tsx index 27b10c15..5514e828 100644 --- a/src/vim-web/react-viewers/panels/axesPanel.tsx +++ b/src/vim-web/react-viewers/panels/axesPanel.tsx @@ -6,14 +6,15 @@ import React, { useEffect, useRef, useState } from 'react' import * as Core from '../../core-viewers' import * as Icons from '../icons' import { CameraRef } from '../state/cameraState' -import { isTrue, Settings } from '../settings' import { SettingsState } from '../settings/settingsState' import { whenAllTrue, whenTrue } from '../helpers/utils' +import { WebglSettings } from '../webgl/settings' +import { isTrue } from '../settings/userBoolean' -function anyUiAxesButton (settings: Settings) { +function anyUiAxesButton (settings: WebglSettings) { return ( - settings.ui.orthographic || - settings.ui.resetCamera + settings.ui.axesOrthographic || + settings.ui.axesHome ) } @@ -25,7 +26,7 @@ export const AxesPanelMemo = React.memo(AxesPanel) /** * JSX Component for axes gizmo. */ -function AxesPanel (props: { viewer: Core.Webgl.Viewer, camera: CameraRef, settings: SettingsState }) { +function AxesPanel (props: { viewer: Core.Webgl.Viewer, camera: CameraRef, settings: SettingsState }) { const viewer = props.viewer const [ortho, setOrtho] = useState(viewer.camera.orthographic) @@ -97,7 +98,7 @@ function AxesPanel (props: { viewer: Core.Webgl.Viewer, camera: CameraRef, setti ) - const hidden = isTrue(props.settings.value.ui.axesPanel) ? '' : ' vc-hidden' + const hidden = isTrue(props.settings.value.ui.panelAxes) ? '' : ' vc-hidden' const empty = !anyUiAxesButton(props.settings.value) const createBar = () => { @@ -106,9 +107,9 @@ function AxesPanel (props: { viewer: Core.Webgl.Viewer, camera: CameraRef, setti
{whenAllTrue([ - props.settings.value.ui.orthographic + props.settings.value.ui.axesOrthographic ], btnOrtho)} - {whenTrue(props.settings.value.ui.resetCamera, btnHome)} + {whenTrue(props.settings.value.ui.axesHome, btnHome)}
) diff --git a/src/vim-web/react-viewers/panels/isolationPanel.tsx b/src/vim-web/react-viewers/panels/isolationPanel.tsx index c61b46ef..1f877b78 100644 --- a/src/vim-web/react-viewers/panels/isolationPanel.tsx +++ b/src/vim-web/react-viewers/panels/isolationPanel.tsx @@ -5,9 +5,10 @@ import { GenericPanel, GenericPanelHandle } from "../generic/genericPanel"; export const Ids = { showGhost: "isolationPanel.showGhost", ghostOpacity: "isolationPanel.ghostOpacity", + transparency: "isolationPanel.transparency", } -export const IsolationPanel = forwardRef( +export const IsolationPanel = forwardRef( (props, ref) => { return ( props.state.showGhost.get(), min: 0, max: 1, - step: 0.05 }, + step: 0.05 + }, + { + type: "bool", + visible: () => props.transparency, + id: Ids.transparency, + label: "Transparency", + state: props.state.transparency + }, ]} /> ); diff --git a/src/vim-web/react-viewers/panels/sidePanel.tsx b/src/vim-web/react-viewers/panels/sidePanel.tsx index d882d4d9..e713cc40 100644 --- a/src/vim-web/react-viewers/panels/sidePanel.tsx +++ b/src/vim-web/react-viewers/panels/sidePanel.tsx @@ -22,7 +22,7 @@ export const SidePanelMemo = React.memo(SidePanel) export function SidePanel (props: { container: Container side: SideState - viewer: Core.Webgl.Viewer + viewer: Core.Webgl.Viewer | Core.Ultra.Viewer content: () => JSX.Element }) { const resizeTimeOut = useRef() @@ -36,7 +36,7 @@ export function SidePanel (props: { props.container.gfx.style.left = '0px' } - props.viewer.viewport.ResizeToParent() + props.viewer.viewport.resizeToParent() } const getMaxSize = () => { @@ -104,9 +104,10 @@ export function SidePanel (props: { style={{ position: 'absolute' }} - className={`vim-side-panel vc-top-0 vc-left-0 vc-z-20 vc-bg-gray-lightest vc-text-gray-darker ${ - props.side.getContent() !== 'none' ? '' : 'vc-hidden' - }`} + className={`vim-side-panel vc-top-0 vc-left-0 vc-z-20 + vc-bg-gray-lightest vc-text-gray-darker + vc-border-r vc-border-gray-light + ${props.side.getContent() !== 'none' ? '' : 'vc-hidden'}`} >

diff --git a/src/vim-web/react-viewers/ultra/errors/serverFileDownloadingError.tsx b/src/vim-web/react-viewers/ultra/errors/serverFileDownloadingError.tsx index 1e691ab4..eb2367c8 100644 --- a/src/vim-web/react-viewers/ultra/errors/serverFileDownloadingError.tsx +++ b/src/vim-web/react-viewers/ultra/errors/serverFileDownloadingError.tsx @@ -12,7 +12,7 @@ export function serverFileDownloadingError (url : string, authToken?: string, se return { title: 'VIM Ultra Download Error', body: body(url, authToken, server), - footer: style.footer(Urls.support), + footer: style.footer(), canClose: false } } diff --git a/src/vim-web/react-viewers/ultra/errors/serverStreamError.tsx b/src/vim-web/react-viewers/ultra/errors/serverStreamError.tsx index 303df6d5..434b6e9c 100644 --- a/src/vim-web/react-viewers/ultra/errors/serverStreamError.tsx +++ b/src/vim-web/react-viewers/ultra/errors/serverStreamError.tsx @@ -6,7 +6,7 @@ export function serverStreamError (url: string): MessageBoxProps { return { title: 'VIM Ultra Stream Error', body: body(url), - footer: style.footer(Urls.support), + footer: style.footer(), canClose: false } } diff --git a/src/vim-web/react-viewers/ultra/index.ts b/src/vim-web/react-viewers/ultra/index.ts index daa5a5cd..4c885760 100644 --- a/src/vim-web/react-viewers/ultra/index.ts +++ b/src/vim-web/react-viewers/ultra/index.ts @@ -1,2 +1,3 @@ export * from './viewer' export * from './viewerRef' +export * from './settings' diff --git a/src/vim-web/react-viewers/ultra/isolation.ts b/src/vim-web/react-viewers/ultra/isolation.ts index 1d211b70..70c2b1b3 100644 --- a/src/vim-web/react-viewers/ultra/isolation.ts +++ b/src/vim-web/react-viewers/ultra/isolation.ts @@ -111,6 +111,9 @@ function createAdapter(viewer: Viewer): IsolationAdapter { } } }, + enableTransparency: (enable: boolean) => { + console.log("enableTransparency not implemented") + }, getGhostOpacity: () => viewer.renderer.ghostOpacity, setGhostOpacity: (opacity: number) => { diff --git a/src/vim-web/react-viewers/ultra/settings.ts b/src/vim-web/react-viewers/ultra/settings.ts new file mode 100644 index 00000000..f3d8a01f --- /dev/null +++ b/src/vim-web/react-viewers/ultra/settings.ts @@ -0,0 +1,62 @@ +import { RecursivePartial } from "../helpers/utils" +import { UserBoolean } from "../settings/userBoolean" +import { ControlBarCameraSettings, ControlBarCursorSettings, ControlBarMeasureSettings, ControlBarSectionBoxSettings, ControlBarVisibilitySettings } from "../state/controlBarState" + +export type PartialUltraSettings = RecursivePartial + +export type UltraSettings = { + ui: ControlBarCameraSettings & + ControlBarCursorSettings & + ControlBarSectionBoxSettings & + ControlBarVisibilitySettings & { + // Panels + panelLogo: UserBoolean + panelControlBar: UserBoolean + + // Control bar - misc + miscSettings: UserBoolean + miscHelp: UserBoolean + } +} + +export function getDefaultUltraSettings(): UltraSettings { + return { + + ui: { + // panels + panelLogo: true, + panelControlBar: true, + + // Control bar - cursors + cursorOrbit: true, + cursorLookAround: true, + cursorPan: true, + cursorZoom: true, + + // Control bar - camera + cameraAuto: true, + cameraFrameScene: true, + cameraFrameSelection: true, + + // Control bar - tools + sectioningEnable: true, + sectioningFitToSelection: true, + sectioningReset: true, + sectioningShow : true, + sectioningAuto : true, + sectioningSettings : true, + + // Control bar - Visibility + visibilityClearSelection: true, + visibilityShowAll: true, + visibilityToggle: true, + visibilityIsolate: true, + visibilityAutoIsolate: true, + visibilitySettings: true, + + // Control bar - misc + miscSettings: true, + miscHelp: true, + } + } +} \ No newline at end of file diff --git a/src/vim-web/react-viewers/ultra/settingsPanel.ts b/src/vim-web/react-viewers/ultra/settingsPanel.ts new file mode 100644 index 00000000..279e0ef2 --- /dev/null +++ b/src/vim-web/react-viewers/ultra/settingsPanel.ts @@ -0,0 +1,42 @@ +import { Viewer } from "../../core-viewers/ultra"; +import { SettingsItem } from "../settings/settingsItem"; +import { SettingsPanelKeys } from "../settings/settingsKeys"; +import { getControlBarCameraSettings, getControlBarSectionBoxSettings, getControlBarVisibilitySettings } from "../settings/settingsPanelContent"; +import { UltraSettings } from "./settings"; + +export function getControlBarUltraSettings(): SettingsItem[] { + return [ + { + type: 'subtitle', + key: SettingsPanelKeys.ControlBarMiscSubtitle, + title: 'Control Bar - Settings', + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarMiscShowSettingsButtonToggle, + label: 'Settings', + getter: (s) => s.ui.miscSettings, + setter: (s, v) => (s.ui.miscSettings = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarMiscShowHelpButtonToggle, + label: 'Help', + getter: (s) => s.ui.miscHelp, + setter: (s, v) => (s.ui.miscHelp = v), + }, + ] +} + + // Ultra: only control bar–related sections +export function getUltraSettingsContent( + viewer: Viewer, +): SettingsItem[] { + // viewer kept for a consistent signature, in case you need it later + return [ + ...getControlBarCameraSettings(), + ...getControlBarVisibilitySettings(), + ...getControlBarSectionBoxSettings(), + ...getControlBarUltraSettings(), + ] +} diff --git a/src/vim-web/react-viewers/ultra/viewer.tsx b/src/vim-web/react-viewers/ultra/viewer.tsx index 93e83ab3..2ccc19c1 100644 --- a/src/vim-web/react-viewers/ultra/viewer.tsx +++ b/src/vim-web/react-viewers/ultra/viewer.tsx @@ -1,7 +1,8 @@ -import * as Core from '../../core-viewers' -import React, {useRef, RefObject, useEffect, useState } from 'react' +import * as Core from '../../core-viewers' +import { useSettings } from '../../react-viewers/settings/settingsState' +import {useRef, RefObject, useEffect, useState } from 'react' import { Container, createContainer } from '../container' import { createRoot } from 'react-dom/client' import { Overlay } from '../panels/overlay' @@ -24,6 +25,13 @@ import { useUltraIsolation } from './isolation' import { IsolationPanel } from '../panels/isolationPanel' import { GenericPanelHandle } from '../generic/genericPanel' import { ControllablePromise } from '../../utils' +import { SettingsPanel } from '../settings/settingsPanel' +import { SidePanelMemo } from '../panels/sidePanel' +import { getDefaultUltraSettings, PartialUltraSettings, UltraSettings } from './settings' +import { getUltraSettingsContent } from './settingsPanel' +import { SettingsCustomizer } from '../settings/settingsItem' +import { isTrue } from '../settings/userBoolean' + /** * Creates a UI container along with a VIM.Viewer and its associated React viewer. @@ -31,8 +39,10 @@ import { ControllablePromise } from '../../utils' * @returns An object containing the resulting container, reactRoot, and viewer. */ export function createViewer ( - container?: Container | HTMLElement + container?: Container | HTMLElement, + settings?: PartialUltraSettings ) : Promise { + const controllablePromise = new ControllablePromise() const cmpContainer = container instanceof HTMLElement ? createContainer(container) @@ -58,6 +68,7 @@ export function createViewer ( controllablePromise.resolve(attachDispose(cmp))} /> ) @@ -74,9 +85,10 @@ export function createViewer ( export function Viewer (props: { container: Container core: Core.Ultra.Viewer + settings?: PartialUltraSettings onMount: (viewer: ViewerRef) => void}) { - + const settings = useSettings(props.settings ?? {}, getDefaultUltraSettings()) const sectionBoxRef = useUltraSectionBox(props.core) const camera = useUltraCamera(props.core, sectionBoxRef) const isolationPanelHandle = useRef(null) @@ -87,7 +99,16 @@ export function Viewer (props: { const [_, setSelectState] = useState(0) const [controlBarCustom, setControlBarCustom] = useState(() => c => c) const isolationRef = useUltraIsolation(props.core) - const controlBar = useUltraControlBar(props.core, sectionBoxRef, isolationRef, camera, _ =>_) + const controlBar = useUltraControlBar( + props.core, + sectionBoxRef, + isolationRef, + camera, + settings.value, + side, + modalHandle.current, + _ =>_ + ) useViewerInput(props.core.inputs, camera) @@ -115,6 +136,11 @@ export function Viewer (props: { isolation: isolationRef, sectionBox: sectionBoxRef, camera, + settings: { + update : settings.update, + register : settings.register, + customize : (c: SettingsCustomizer) => settings.customizer.set(c) + }, get isolationPanel(){ return isolationPanelHandle.current }, @@ -129,17 +155,33 @@ export function Viewer (props: { }) }, []) + const sidePanel = () => ( + <> + + + ) + return <> + { return <> - {whenTrue(true, )} + {whenTrue(settings.value.ui.panelLogo, )} - + }}/> diff --git a/src/vim-web/react-viewers/ultra/viewerRef.ts b/src/vim-web/react-viewers/ultra/viewerRef.ts index df5eade2..99cac71a 100644 --- a/src/vim-web/react-viewers/ultra/viewerRef.ts +++ b/src/vim-web/react-viewers/ultra/viewerRef.ts @@ -6,6 +6,8 @@ import { SectionBoxRef } from '../state/sectionBoxState'; import { IsolationRef } from '../state/sharedIsolation'; import { ControlBarRef } from '../controlbar'; import { GenericPanelHandle } from '../generic/'; +import { SettingsRef } from '../webgl'; +import { UltraSettings } from './settings'; export type ViewerRef = { /** @@ -34,6 +36,8 @@ export type ViewerRef = { camera: CameraRef isolation: IsolationRef + + settings: SettingsRef /** * API to interact with the isolation panel. diff --git a/src/vim-web/react-viewers/urls.ts b/src/vim-web/react-viewers/urls.ts index 7db6bd76..160aac59 100644 --- a/src/vim-web/react-viewers/urls.ts +++ b/src/vim-web/react-viewers/urls.ts @@ -1,4 +1,3 @@ -export const support = 'https://docs.vimaec.com' export const supportUltra = 'https://docs.vimaec.com/docs/vim-for-windows/configuring-vim-ultra' export const supportControls = 'https://docs.vimaec.com/docs/vim-cloud/webgl-navigation-and-controls-guide' diff --git a/src/vim-web/react-viewers/webgl/index.ts b/src/vim-web/react-viewers/webgl/index.ts index 73b57ba1..4cf53ed0 100644 --- a/src/vim-web/react-viewers/webgl/index.ts +++ b/src/vim-web/react-viewers/webgl/index.ts @@ -2,6 +2,7 @@ // Full exports export * from './viewer'; export * from './viewerRef'; +export * from './settings' // Type exports export type * from './loading'; diff --git a/src/vim-web/react-viewers/webgl/isolation.ts b/src/vim-web/react-viewers/webgl/isolation.ts index 1a7c3827..6b322d64 100644 --- a/src/vim-web/react-viewers/webgl/isolation.ts +++ b/src/vim-web/react-viewers/webgl/isolation.ts @@ -1,4 +1,5 @@ import * as Core from "../../core-viewers"; +import { Element3D, Selectable } from "../../core-viewers/webgl"; import { IsolationAdapter, useSharedIsolation as useSharedIsolation, VisibilityStatus } from "../state/sharedIsolation"; export function useWebglIsolation(viewer: Core.Webgl.Viewer){ @@ -7,7 +8,35 @@ export function useWebglIsolation(viewer: Core.Webgl.Viewer){ } function createWebglIsolationAdapter(viewer: Core.Webgl.Viewer): IsolationAdapter { - + var transparency: boolean = true; + var ghost: boolean = false; + var rooms: boolean = false; + + function updateMaterials(){ + viewer.renderer.modelMaterial = + !ghost && transparency ? undefined + : ghost && transparency ? [undefined, viewer.materials.ghost] + : !ghost && !transparency ? viewer.materials.simple + : ghost && !transparency ? [viewer.materials.simple, viewer.materials.ghost] + : (() => { throw new Error("Unreachable state in isolation materials") })(); + } + + function updateVisibility(elements: 'all' | Selectable[], predicate: (object: Selectable) => boolean){ + if(elements === 'all'){ + for(let v of viewer.vims){ + for(let o of v.getAllElements()){ + if(o.type === 'Element3D'){ + o.visible = o.isRoom ? rooms : predicate(o); + } + } + } + } else { + for(let o of elements){ + o.visible = o.isRoom ? rooms : predicate(o); + } + } + } + return { onVisibilityChange: viewer.renderer.onSceneUpdated, onSelectionChanged: viewer.selection.onSelectionChanged, @@ -18,30 +47,30 @@ function createWebglIsolationAdapter(viewer: Core.Webgl.Viewer): IsolationAdapte clearSelection: () => viewer.selection.clear(), - isolateSelection: () => updateAllVisibility(viewer, o => viewer.selection.has(o)), + isolateSelection: () => updateVisibility('all', o => viewer.selection.has(o)), hideSelection: () => { - viewer.selection.getAll().forEach(o => o.visible = false) + updateVisibility(viewer.selection.getAll(), o => false) }, showSelection: () => { - viewer.selection.getAll().forEach(o => o.visible = true) + updateVisibility(viewer.selection.getAll(), o => true) }, hideAll: () => { - updateAllVisibility(viewer, o => false) + updateVisibility('all', o => false) }, showAll: () => { - updateAllVisibility(viewer, o => true) + updateVisibility('all', o => true) }, isolate: (instances: number[]) => { const set = new Set(instances) - updateAllVisibility(viewer, o => o.instances.some(i => set.has(i))) + updateVisibility('all', o => o.instances.some(i => set.has(i))) }, show: (instances: number[]) => { for(let i of instances){ for(let v of viewer.vims){ const o = v.getElement(i) - o.visible = true + o.visible = o.isRoom ? rooms : true } } }, @@ -50,36 +79,34 @@ function createWebglIsolationAdapter(viewer: Core.Webgl.Viewer): IsolationAdapte for(let i of instances){ for(let v of viewer.vims){ const o = v.getElement(i) - o.visible = false; + o.visible = o.isRoom ? rooms : false } } }, - - showGhost: (show: boolean) => { - viewer.renderer.modelMaterial = show - ? [viewer.materials.simple, viewer.materials.ghost] - : undefined + enableTransparency: (enable: boolean) => { + if(transparency !== enable){ + transparency = enable; + updateMaterials(); + }; }, + showGhost: (show: boolean) => { + ghost = show; + updateMaterials(); + }, + getGhostOpacity: () => viewer.materials.ghostOpacity, setGhostOpacity: (opacity: number) => viewer.materials.ghostOpacity = opacity, - getShowRooms: () => true, - setShowRooms: (show: boolean) => {console.log("setShowRooms not implemented")}, - - - }; -} - -function updateAllVisibility(viewer: Core.Webgl.Viewer, predicate: (object: Core.Webgl.Element3D) => boolean){ - for(let v of viewer.vims){ - for(let o of v.getAllElements()){ - if(o.type === 'Element3D'){ - o.visible = predicate(o) + getShowRooms: () => rooms, + setShowRooms: (show: boolean) => { + if(rooms !== show){ + rooms = show; + updateVisibility('all', o => o.visible);arguments } - } - } + }, + }; } function getVisibilityState(viewer: Core.Webgl.Viewer): VisibilityStatus { diff --git a/src/vim-web/react-viewers/webgl/loading.ts b/src/vim-web/react-viewers/webgl/loading.ts index 7edecba9..fc71ccbd 100644 --- a/src/vim-web/react-viewers/webgl/loading.ts +++ b/src/vim-web/react-viewers/webgl/loading.ts @@ -7,7 +7,7 @@ import * as Core from '../../core-viewers' import { LoadRequest } from '../helpers/loadRequest' import { ModalHandle } from '../panels/modal' import { UltraSuggestion } from '../panels/loadingBox' -import { Settings } from '../settings' +import { WebglSettings } from './settings' type AddSettings = { /** @@ -39,7 +39,7 @@ export class ComponentLoader { private _modal: React.RefObject private _addLink : boolean = false - constructor (viewer : Core.Webgl.Viewer, modal: React.RefObject, settings: Settings) { + constructor (viewer : Core.Webgl.Viewer, modal: React.RefObject, settings: WebglSettings) { this._viewer = viewer this._modal = modal // TODO: Enable this when we are ready to support it diff --git a/src/vim-web/react-viewers/webgl/settings.ts b/src/vim-web/react-viewers/webgl/settings.ts new file mode 100644 index 00000000..58bd49b9 --- /dev/null +++ b/src/vim-web/react-viewers/webgl/settings.ts @@ -0,0 +1,106 @@ +import { RecursivePartial } from "../../utils" +import { UserBoolean } from "../settings/userBoolean" +import { ControlBarCameraSettings, ControlBarCursorSettings, ControlBarMeasureSettings, ControlBarSectionBoxSettings, ControlBarVisibilitySettings } from "../state/controlBarState" + +export type PartialWebglSettings = RecursivePartial + +/** + * Complete settings configuration for the Vim viewer + * @interface Settings + */ +export type WebglSettings = { + capacity: { + canFollowUrl: boolean + canGoFullScreen: boolean + canDownload: boolean + canReadLocalStorage: boolean + } + ui: ControlBarCameraSettings & + ControlBarCursorSettings & + ControlBarSectionBoxSettings & + ControlBarVisibilitySettings & + ControlBarMeasureSettings & { + + // panels + panelLogo: UserBoolean + panelBimTree: UserBoolean + panelBimInfo: UserBoolean + panelPerformance: UserBoolean + panelAxes: UserBoolean + panelControlBar: UserBoolean + + // axesPanel + axesOrthographic: UserBoolean + axesHome: UserBoolean + + // Control bar - settings + miscProjectInspector: UserBoolean + miscSettings: UserBoolean + miscHelp: UserBoolean + miscMaximise: UserBoolean + } +} + + +/** + * Default settings configuration for the React Webgl Vim viewer + * @constant + * @type {WebglSettings} + */ +export function getDefaultSettings(): WebglSettings { + return { + capacity: { + canFollowUrl: true, + canGoFullScreen: true, + canDownload: true, + canReadLocalStorage: true + }, + ui: { + panelLogo: true, + panelPerformance: false, + panelBimTree: true, + panelBimInfo: true, + panelAxes: true, + panelControlBar: true, + + axesOrthographic: true, + axesHome: true, + + // Control bar - cursors + cursorOrbit: true, + cursorLookAround: true, + cursorPan: true, + cursorZoom: true, + + // Control bar - camera + cameraAuto: true, + cameraFrameScene: true, + cameraFrameSelection: true, + + // Control bar - tools + sectioningEnable: true, + sectioningFitToSelection: true, + sectioningReset: true, + sectioningShow : true, + sectioningAuto : true, + sectioningSettings : true, + + measureEnable: true, + + // Control bar - Visibility + visibilityClearSelection: true, + visibilityShowAll: true, + visibilityToggle: true, + visibilityIsolate: true, + visibilityAutoIsolate: true, + visibilitySettings: true, + + // Control bar - settings + miscProjectInspector: true, + miscSettings: true, + miscHelp: true, + miscMaximise: true + } + } +} + diff --git a/src/vim-web/react-viewers/webgl/settingsPanel.ts b/src/vim-web/react-viewers/webgl/settingsPanel.ts new file mode 100644 index 00000000..8f6faee5 --- /dev/null +++ b/src/vim-web/react-viewers/webgl/settingsPanel.ts @@ -0,0 +1,194 @@ +import { THREE } from "../.."; +import { Viewer } from "../../core-viewers/webgl"; +import { isTrue } from "../settings"; +import { SettingsItem } from "../settings/settingsItem"; +import { SettingsPanelKeys } from "../settings/settingsKeys"; +import { getControlBarCameraSettings, getControlBarCursorSettings, getControlBarSectionBoxSettings, getControlBarVisibilitySettings } from "../settings/settingsPanelContent"; +import { WebglSettings } from "./settings"; + +export function getControlBarVariousSettings(): SettingsItem[] { + return [ + { + type: 'subtitle', + key: SettingsPanelKeys.ControlBarMiscSubtitle, + title: 'Control Bar - Settings', + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarMiscShowProjectInspectorButtonToggle, + label: 'Project Inspector', + getter: (s) => s.ui.miscProjectInspector, + setter: (s, v) => (s.ui.miscProjectInspector = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarMiscShowSettingsButtonToggle, + label: 'Settings', + getter: (s) => s.ui.miscSettings, + setter: (s, v) => (s.ui.miscSettings = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarMiscShowHelpButtonToggle, + label: 'Help', + getter: (s) => s.ui.miscHelp, + setter: (s, v) => (s.ui.miscHelp = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarMiscShowMaximiseButtonToggle, + label: 'Maximise', + getter: (s) => s.ui.miscMaximise, + setter: (s, v) => (s.ui.miscMaximise = v), + }, + ] +} + + + +export function getPanelsVisibilitySettings(): SettingsItem[] { + return [ + { + type: 'subtitle', + key: SettingsPanelKeys.PanelsSubtitle, + title: 'Panels Visibility', + }, + { + type: 'toggle', + key: SettingsPanelKeys.PanelsShowLogoToggle, + label: 'Logo', + getter: (s) => s.ui.panelLogo, + setter: (s, v) => (s.ui.panelLogo = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.PanelsShowBimTreeToggle, + label: 'Bim Tree', + getter: (s) => s.ui.panelBimTree, + setter: (s, v) => (s.ui.panelBimTree = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.PanelsShowBimInfoToggle, + label: 'Bim Info', + getter: (s) => s.ui.panelBimInfo, + setter: (s, v) => (s.ui.panelBimInfo = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.PanelsShowAxesPanelToggle, + label: 'Axes', + getter: (s) => s.ui.panelAxes, + setter: (s, v) => (s.ui.panelAxes = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.PanelsShowPerformancePanelToggle, + label: 'Performance', + getter: (s) => s.ui.panelPerformance, + setter: (s, v) => (s.ui.panelPerformance = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarShowControlBarToggle, + label: 'Control Bar', + getter: (s) => s.ui.panelControlBar, + setter: (s, v) => (s.ui.panelControlBar = v), + }, + ] +} + +export function getInputsSettings( + viewer: Viewer, +): SettingsItem[] { + return [ + { + type: 'subtitle', + key: SettingsPanelKeys.InputsSubtitle, + title: 'Inputs', + }, + { + type: 'box', + key: SettingsPanelKeys.InputsScrollSpeedBox, + label: 'Scroll Speed', + info: '[0.1,10]', + transform: (n) => THREE.MathUtils.clamp(n, 0.1, 10), + getter: (_s) => viewer.inputs.scrollSpeed, + setter: (_s, v) => { + viewer.inputs.scrollSpeed = v + }, + }, + ] +} + +function getAxesPanelSettings(): SettingsItem[] { + return [ + { + type: 'subtitle', + key: SettingsPanelKeys.AxesSubtitle, + title: 'Axes Panel', + }, + { + type: 'toggle', + key: SettingsPanelKeys.AxesShowOrthographicButtonToggle, + label: 'Orthographic Camera', + getter: (s) => s.ui.axesOrthographic, + setter: (s, v) => (s.ui.axesOrthographic = v), + }, + { + type: 'toggle', + key: SettingsPanelKeys.AxesShowResetCameraButtonToggle, + label: 'Reset Camera', + getter: (s) => s.ui.axesHome, + setter: (s, v) => (s.ui.axesHome = v), + }, + ] +} + +export function getControlBarMeasureSettings(): SettingsItem[] { + return [ + { + type: 'subtitle', + key: SettingsPanelKeys.ControlBarToolsSubtitle, + title: 'Control Bar - Measurement', + }, + { + type: 'toggle', + key: SettingsPanelKeys.ControlBarToolsShowMeasuringModeButtonToggle, + label: 'Enable', + getter: (s) => s.ui.measureEnable, + setter: (s, v) => (s.ui.measureEnable = v), + }, + ] +} + +export function getWebglSettingsContent( + viewer: Viewer, +): SettingsItem[] { + return [ + ...getInputsSettings(viewer), + ...getPanelsVisibilitySettings(), + ...getAxesPanelSettings(), + ...getControlBarCursorSettings(), + ...getControlBarCameraSettings(), + ...getControlBarVisibilitySettings(), + ...getControlBarMeasureSettings(), + ...getControlBarSectionBoxSettings(), + ...getControlBarVariousSettings(), + ] +} + +/** + * Apply given vim viewer settings to the given viewer. + */ +export function applyWebglSettings (settings: WebglSettings) { + // Show/Hide performance gizmo + const performance = document.getElementsByClassName('vim-performance-div')[0] + if (performance) { + if (isTrue(settings.ui.panelPerformance)) { + performance.classList.remove('vc-hidden') + } else { + performance.classList.add('vc-hidden') + } + } +} diff --git a/src/vim-web/react-viewers/webgl/viewer.tsx b/src/vim-web/react-viewers/webgl/viewer.tsx index bc1d9a75..0ded9cc0 100644 --- a/src/vim-web/react-viewers/webgl/viewer.tsx +++ b/src/vim-web/react-viewers/webgl/viewer.tsx @@ -19,13 +19,11 @@ import { } from '../panels/contextMenu' import { SidePanelMemo } from '../panels/sidePanel' import { useSideState } from '../state/sideState' -import { SettingsPanel } from '../settings/settingsPanel' import { MenuToastMemo } from '../panels/toast' import { Overlay } from '../panels/overlay' import { addPerformanceCounter } from '../panels/performance' import { applyWebglBindings } from './inputsBindings' import { CursorManager } from '../helpers/cursor' -import { PartialSettings, isTrue } from '../settings' import { useSettings } from '../settings/settingsState' import { TreeActionRef } from '../bim/bimTree' import { Container, createContainer } from '../container' @@ -44,6 +42,11 @@ import { IsolationPanel } from '../panels/isolationPanel' import { useWebglIsolation } from './isolation' import { GenericPanelHandle } from '../generic' import { ControllablePromise } from '../../utils' +import { SettingsCustomizer } from '../settings/settingsItem' +import { getDefaultSettings, PartialWebglSettings, WebglSettings } from './settings' +import { isTrue } from '../settings/userBoolean' +import { SettingsPanel } from '../settings/settingsPanel' +import { applyWebglSettings, getWebglSettingsContent } from './settingsPanel' /** * Creates a UI container along with a VIM.Viewer and its associated React viewer. @@ -54,7 +57,7 @@ import { ControllablePromise } from '../../utils' */ export function createViewer ( container?: Container | HTMLElement, - settings: PartialSettings = {}, + settings: PartialWebglSettings = {}, coreSettings: Core.Webgl.PartialViewerSettings = {} ) : Promise { const controllablePromise = new ControllablePromise() @@ -105,9 +108,9 @@ export function Viewer (props: { container: Container viewer: Core.Webgl.Viewer onMount: (viewer: ViewerRef) => void - settings?: PartialSettings + settings?: PartialWebglSettings }) { - const settings = useSettings(props.viewer, props.settings ?? {}) + const settings = useSettings(props.settings ?? {}, getDefaultSettings(), (s) => applyWebglSettings(s)) const modal = useRef(null) const sectionBoxRef = useWebglSectionBox(props.viewer) @@ -120,8 +123,8 @@ export function Viewer (props: { useViewerInput(props.viewer.inputs, camera) const side = useSideState( - isTrue(settings.value.ui.bimTreePanel) || - isTrue(settings.value.ui.bimInfoPanel), + isTrue(settings.value.ui.panelBimTree) || + isTrue(settings.value.ui.panelBimInfo), Math.min(props.container.root.clientWidth * 0.25, 340) ) const [contextMenu, setcontextMenu] = useState() @@ -132,12 +135,8 @@ export function Viewer (props: { const treeRef = useRef() const performanceRef = useRef(null) const isolationRef = useWebglIsolation(props.viewer) - const controlBar = useControlBar(props.viewer, camera, modal.current, side, cursor, settings.value, sectionBoxRef, isolationRef, controlBarCustom) - - - useEffect(() => { side.setHasBim(viewerState.vim.get()?.bim !== undefined) }) @@ -176,7 +175,11 @@ export function Viewer (props: { loader: loader.current, isolation: isolationRef, camera, - settings, + settings: { + update : settings.update, + register : settings.register, + customize : (c: SettingsCustomizer) => settings.customizer.set(c) + }, get isolationPanel(){ return isolationPanelHandle.current }, @@ -218,7 +221,7 @@ export function Viewer (props: { />} @@ -236,13 +239,13 @@ export function Viewer (props: { { return <> - {whenTrue(settings.value.ui.logo, )} + {whenTrue(settings.value.ui.panelLogo, )} - + = { // Double lambda is required to prevent react from using reducer pattern // https://stackoverflow.com/questions/59040989/usestate-with-a-lambda-invokes-the-lambda-when-set @@ -25,13 +27,19 @@ export type SettingsRef = { * Allows updating settings by providing a callback function. * @param updater A function that updates the current settings. */ - update : (updater: (settings: Settings) => void) => void + update : (updater: (settings: T) => void) => void /** * Registers a callback function to be notified when settings are updated. * @param callback A function to be called when settings are updated, receiving the updated settings. */ - register : (callback: (settings: Settings) => void) => void + register : (callback: (settings: T) => void) => void + + /** + * Customizes the settings panel by providing a customizer function. + * @param customizer A function that modifies the settings items. + */ + customize : (customizer: (items: SettingsItem[]) => SettingsItem[]) => void } @@ -97,7 +105,7 @@ export type ViewerRef = { /** * Settings API managing settings applied to the viewer. */ - settings: SettingsRef + settings: SettingsRef /** * Message API to interact with the loading box.