diff --git a/package.json b/package.json index 1feb86ff..cdd9900a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vim-webgl-component", - "version": "0.3.9", + "version": "0.3.11-dev.0", "description": "A demonstration app built on top of the vim-webgl-viewer", "files": [ "dist" @@ -62,7 +62,7 @@ "react-tooltip": "^4.2.21", "stats-js": "^1.0.1", "tailwindcss-scoped-preflight": "^3.2.8", - "vim-webgl-viewer": "2.0.4-dev.0" + "vim-webgl-viewer": "2.0.5-dev.3" }, "peerDependencies": { "react": "^18.2.0", diff --git a/src/component.tsx b/src/component.tsx index 92cbfa9c..3f982b3d 100644 --- a/src/component.tsx +++ b/src/component.tsx @@ -10,11 +10,12 @@ import 'vim-webgl-viewer/dist/style.css' import * as VIM from 'vim-webgl-viewer/' import { AxesPanelMemo } from './panels/axesPanel' -import { ControlBar, RestOfScreen } from './controlbar/controlBar' +import { ControlBar, ControlBarCustomization } from './controlbar/controlBar' +import { RestOfScreen } from './controlbar/restOfScreen' import { LoadingBoxMemo, MsgInfo, ComponentLoader } from './panels/loading' import { OptionalBimPanel } from './bim/bimPanel' import { - contextMenuCustomization, + ContextMenuCustomization, showContextMenu, VimContextMenuMemo } from './panels/contextMenu' @@ -38,11 +39,14 @@ import { LogoMemo } from './panels/logo' import { VimComponentRef } from './vimComponentRef' import { createBimInfoState } from './bim/bimInfoData' import { whenTrue } from './helpers/utils' +import { DeferredPromise } from './helpers/deferredPromise' export * as VIM from 'vim-webgl-viewer/' export const THREE = VIM.THREE export * as ContextMenu from './panels/contextMenu' export * as BimInfo from './bim/bimInfoData' +export * as ControlBar from './controlbar/controlBar' +export * as Icons from './panels/icons' export * from './vimComponentRef' export { getLocalComponentSettings as getLocalSettings } from './settings/settingsStorage' export { type ComponentSettings as Settings, type PartialComponentSettings as PartialSettings, defaultSettings } from './settings/settings' @@ -56,24 +60,39 @@ export * from './container' * @returns An object containing the resulting container, reactRoot, and viewer. */ export function createVimComponent ( - onMount: (component: VimComponentRef) => void, container?: VimComponentContainer, componentSettings: PartialComponentSettings = {}, viewerSettings: VIM.PartialViewerSettings = {} -) { +) : Promise { + const promise = new DeferredPromise() + + // Create the viewer and container const viewer = new VIM.Viewer(viewerSettings) container = container ?? createContainer() viewer.viewport.reparent(container.gfx) + + // Create the React root const reactRoot = createRoot(container.ui) + + // Patch the component to clean up after itself + const patchRef = (cmp : VimComponentRef) => { + cmp.dispose = () => { + viewer.dispose() + container.dispose() + reactRoot.unmount() + } + return cmp + } + reactRoot.render( promise.resolve(patchRef(cmp))} settings={componentSettings} /> ) - return { container, reactRoot, viewer } + return promise } /** @@ -102,7 +121,8 @@ export function VimComponent (props: { isTrue(settings.value.ui.bimInfoPanel), Math.min(props.container.root.clientWidth * 0.25, 340) ) - const [contextMenu, setcontextMenu] = useState() + const [contextMenu, setcontextMenu] = useState() + const [controlBar, setControlBar] = useState() const bimInfoRef = createBimInfoState() const help = useHelp() @@ -144,11 +164,15 @@ export function VimComponent (props: { contextMenu: { customize: (v) => setcontextMenu(() => v) }, + controlBar: { + customize: (v) => setControlBar(() => v) + }, message: { show: (message: string, info: string) => setMsg({ message, info }), hide: () => setMsg(undefined) }, - bimInfo: bimInfoRef + bimInfo: bimInfoRef, + dispose: () => {} }) // Clean up @@ -200,6 +224,7 @@ export function VimComponent (props: { isolation={isolation} cursor={cursor} settings={settings.value} + customization={controlBar} /> void } /** @@ -49,5 +51,17 @@ export function createContainer (element?: HTMLElement): VimComponentContainer { root.append(gfx) root.append(ui) - return { root, ui, gfx } + const dispose = () => { + if (element === undefined) { + // We own the element, so we remove it + root.remove() + } else { + root.classList.remove('vim-component') + // We don't own the element, so we just remove our children + gfx.remove() + ui.remove() + } + } + + return { root, ui, gfx, dispose } } diff --git a/src/controlbar/controlBar.tsx b/src/controlbar/controlBar.tsx index 8beb9f9c..3423c26f 100644 --- a/src/controlbar/controlBar.tsx +++ b/src/controlbar/controlBar.tsx @@ -2,11 +2,10 @@ * @module viw-webgl-component */ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect } from 'react' import ReactTooltip from 'react-tooltip' import * as VIM from 'vim-webgl-viewer/' import { ComponentCamera } from '../helpers/camera' -import { CameraObserver } from '../helpers/cameraObserver' import { CursorManager } from '../helpers/cursor' import { Isolation } from '../helpers/isolation' import { HelpState } from '../panels/help' @@ -14,12 +13,67 @@ import { ComponentSettings, anyUiCursorButton, anyUiSettingButton, - anyUiToolButton + anyUiToolButton, + isTrue } from '../settings/settings' import { SideState } from '../sidePanel/sideState' -import { TabCamera } from './controlBarCamera' -import { TabSettings } from './controlBarSettings' -import { TabTools } from './controlBarTools' +import * as Icons from '../panels/icons' +import { buttonBlueStyle, buttonDefaultStyle } from './controlBarButton' +import { createSection, IControlBarSection, sectionDefaultStyle, sectionBlueStyle } from './controlBarSection' + +import { getPointerState } from './pointerState' +import { getFullScreenState } from './fullScreenState' +import { getSectionBoxState } from './sectionBoxState' +import { getMeasureState } from './measureState' + +export { buttonDefaultStyle, buttonBlueStyle } from './controlBarButton' +export { sectionDefaultStyle, sectionBlueStyle } from './controlBarSection' + +/** + * A map function that changes the context menu. + */ +export type ControlBarCustomization = ( + e: IControlBarSection[] +) => IControlBarSection[] + +export const elementIds = { + // Sections + sectionCamera: 'controlBar.sectionCamera', + sectionTools: 'controlBar.sectionTools', + sectionSettings: 'controlBar.sectionSettings', + sectionMeasure: 'controlBar.sectionMeasure', + sectionSectionBox: 'controlBar.sectionSectionBox', + + // Camera buttons + buttonCameraOrbit: 'controlBar.camera.orbit', + buttonCameraLook: 'controlBarcamera.look', + buttonCameraPan: 'controlBar.camera.pan', + buttonCameraZoom: 'controlBar.camera.zoom', + buttonCameraZoomWindow: 'controlBar.camera.zoomWindow', + buttonCameraZoomToFit: 'controlBar.camera.zoomToFit', + + // Settings buttons + buttonProjectInspector: 'controlBar.projectInspector', + buttonSettings: 'controlBar.settings', + buttonHelp: 'controlBar.help', + buttonMaximize: 'controlBar.maximize', + + // Tools buttons + buttonSectionBox: 'controlBar.sectionBox', + buttonMeasure: 'controlBar.measure', + buttonToggleIsolation: 'controlBar.toggleIsolation', + + // Measure buttons + buttonMeasureDelete: 'controlBar.measure.delete', + buttonMeasureDone: 'controlBar.measure.done', + + // Section box buttons + buttonSectionBoxReset: 'controlBar.sectionBox.reset', + buttonSectionBoxShrinkToSelection: 'controlBar.sectionBox.shrinkToSelection', + buttonSectionBoxClip: 'controlBar.sectionBox.clip', + buttonSectionBoxIgnore: 'controlBar.sectionBox.ignore', + buttonSectionBoxDone: 'controlBar.sectionBox.done' +} /** * JSX Component for the control bar. @@ -32,66 +86,229 @@ export function ControlBar (props: { isolation: Isolation cursor: CursorManager settings: ComponentSettings + customization: ControlBarCustomization }) { - // eslint-disable-next-line no-unused-vars - const [show, setShow] = useState(true) - const cameraObserver = useRef() + const pointer = getPointerState(props.viewer) + const fullScreen = getFullScreenState() + const section = getSectionBoxState(props.viewer) + const measure = getMeasureState(props.viewer, props.cursor) // On Each Render useEffect(() => { ReactTooltip.rebuild() }) - useEffect(() => { - cameraObserver.current = new CameraObserver(props.viewer, 400) - cameraObserver.current.onChange = (moving) => setShow(!moving) + const cameraSection : IControlBarSection = { + id: elementIds.sectionCamera, + enable: () => anyUiCursorButton(props.settings), + style: sectionDefaultStyle, + buttons: [ + { + id: elementIds.buttonCameraOrbit, + enabled: () => isTrue(props.settings.ui.orbit), + tip: 'Orbit', + action: () => pointer.onButton('orbit'), + icon: Icons.orbit, + isOn: () => pointer.mode === 'orbit', + style: buttonDefaultStyle + }, + { + id: elementIds.buttonCameraLook, + enabled: () => isTrue(props.settings.ui.lookAround), + tip: 'Look Around', + action: () => pointer.onButton('look'), + icon: Icons.look, + isOn: () => pointer.mode === 'look', + style: buttonDefaultStyle + }, + { + id: elementIds.buttonCameraPan, + enabled: () => isTrue(props.settings.ui.pan), + tip: 'Pan', + action: () => pointer.onButton('pan'), + icon: Icons.pan, + isOn: () => pointer.mode === 'pan', + style: buttonDefaultStyle + }, + { + id: elementIds.buttonCameraZoom, + enabled: () => isTrue(props.settings.ui.zoom), + tip: 'Zoom', + action: () => pointer.onButton('zoom'), + icon: Icons.zoom, + isOn: () => pointer.mode === 'zoom', + style: buttonDefaultStyle + }, + { + id: elementIds.buttonCameraZoomWindow, + enabled: () => isTrue(props.settings.ui.zoomWindow), + tip: 'Zoom Window', + action: () => { + pointer.onButton('rect') + section.hide() + }, + icon: Icons.frameRect, + isOn: () => pointer.mode === 'rect', + style: buttonDefaultStyle + }, + { + id: elementIds.buttonCameraZoomToFit, + enabled: () => isTrue(props.settings.ui.zoomToFit), + tip: 'Zoom to Fit', + action: () => props.camera.frameContext(), + icon: Icons.frameSelection, + isOn: () => false, + style: buttonDefaultStyle + } + ] + } + + const settingsSection : IControlBarSection = { + id: elementIds.sectionSettings, + enable: () => anyUiSettingButton(props.settings), + style: sectionDefaultStyle, + buttons: [ + { + id: elementIds.buttonProjectInspector, + enabled: () => isTrue(props.settings.ui.projectInspector) && ( + isTrue(props.settings.ui.bimTreePanel) || + isTrue(props.settings.ui.bimInfoPanel)), + tip: 'Project Inspector', + action: () => props.side.toggleContent('bim'), + icon: Icons.treeView, + style: buttonDefaultStyle + }, + { + id: elementIds.buttonSettings, + enabled: () => isTrue(props.settings.ui.settings), + tip: 'Settings', + action: () => props.side.toggleContent('settings'), + icon: Icons.settings, + style: buttonDefaultStyle + }, + { + id: elementIds.buttonHelp, + enabled: () => isTrue(props.settings.ui.help), + tip: 'Help', + action: () => props.help.setVisible(!props.help.visible), + icon: Icons.help, + style: buttonDefaultStyle + }, + { + id: elementIds.buttonMaximize, + enabled: () => + isTrue(props.settings.ui.maximise) && + props.settings.capacity.canGoFullScreen, + tip: fullScreen.get() ? 'Minimize' : 'Fullscreen', + action: () => fullScreen.toggle(), + icon: fullScreen.get() ? Icons.minimize : Icons.fullsScreen, + style: buttonDefaultStyle + } + ] + } + + const sectionBoxSection : IControlBarSection = { + id: elementIds.sectionSectionBox, + enable: () => !measure.active && section.active, + style: sectionBlueStyle, + buttons: [ + { + id: elementIds.buttonSectionBoxReset, + tip: 'Reset Section Box', + action: () => section.reset(), + icon: Icons.sectionBoxReset, + style: buttonBlueStyle + }, + { + id: elementIds.buttonSectionBoxShrinkToSelection, + tip: 'Shrink to Selection', + action: () => section.shrinkToSelection(), + icon: Icons.sectionBoxShrink, + style: buttonBlueStyle + }, + { + id: elementIds.buttonSectionBoxClip, + tip: section.clip ? 'Clip Section Box' : 'Ignore Section Box', + action: () => section.toggleClip(), + icon: section.clip ? Icons.sectionBoxClip : Icons.sectionBoxNoClip, + style: buttonBlueStyle + }, + { + id: elementIds.buttonSectionBoxDone, + tip: 'Done', + action: () => section.toggle(), + icon: Icons.checkmark, + style: buttonBlueStyle + } + ] + } - return () => { - cameraObserver.current?.dispose() - } - }, []) + const measureSection : IControlBarSection = { + id: elementIds.sectionMeasure, + enable: () => measure.active && !section.active, + style: sectionBlueStyle, + buttons: [ + { + id: elementIds.buttonMeasureDelete, + tip: 'Delete', + action: () => measure.clear(), + icon: Icons.trash, + style: buttonBlueStyle + }, + { + id: elementIds.buttonMeasureDone, + tip: 'Done', + action: () => measure.toggle(), + icon: Icons.checkmark, + style: buttonBlueStyle + } + ] + } + const toolSections: IControlBarSection = { + id: elementIds.sectionTools, + enable: () => anyUiToolButton(props.settings) && !measure.active && !section.active, + style: measure.active || section.active ? sectionBlueStyle : sectionDefaultStyle, + buttons: [ + { + id: elementIds.buttonSectionBox, + enabled: () => isTrue(props.settings.ui.sectioningMode), + tip: 'Sectioning Mode', + action: () => section.toggle(), + icon: Icons.sectionBox, + style: buttonDefaultStyle + }, + { + id: elementIds.buttonMeasure, + enabled: () => isTrue(props.settings.ui.measuringMode), + tip: 'Measuring Mode', + action: () => measure.toggle(), + icon: Icons.measure, + style: buttonDefaultStyle + }, + { + id: elementIds.buttonToggleIsolation, + enabled: () => isTrue(props.settings.ui.toggleIsolation), + tip: 'Toggle Isolation', + action: () => props.isolation.toggleIsolation('controlBar'), + icon: Icons.toggleIsolation, + style: buttonDefaultStyle + } + ] + } + + // Apply user customization + let controlBar = [cameraSection, toolSections, measureSection, sectionBoxSection, settingsSection] + controlBar = props.customization?.(controlBar) ?? controlBar + + return createBar(controlBar) +} + +function createBar (sections: IControlBarSection[]) { return
- {anyUiCursorButton(props.settings) ? : null} - {anyUiToolButton(props.settings) ? : null} - {anyUiSettingButton(props.settings) ? : null} + }} className='vim-control-bar vc-pointer-events-auto vc-flex-wrap vc-mx-2 vc-min-w-0 vc-absolute vc-left-0 vc-right-0 vc-z-20 vc-flex vc-items-center vc-justify-center transition-all'> + {sections.map(createSection)}
} - -export function RestOfScreen (props:{ - side: SideState, - content: () => JSX.Element -}) { - const [, setVersion] = useState(0) - const resizeObserver = useRef() - - // On Each Render - useEffect(() => { - ReactTooltip.rebuild() - }) - - useEffect(() => { - resizeObserver.current = new ResizeObserver(() => { - setVersion((prev) => prev ^ 1) - }) - resizeObserver.current.observe(document.body) - - return () => { - resizeObserver.current?.disconnect() - } - }, []) - - return ( -
- {props.content()} -
) -} diff --git a/src/controlbar/controlBarButton.tsx b/src/controlbar/controlBarButton.tsx index 591b8e5c..06039d73 100644 --- a/src/controlbar/controlBarButton.tsx +++ b/src/controlbar/controlBarButton.tsx @@ -1,42 +1,33 @@ import React from 'react' const btnStyle = 'vim-control-bar-button vc-rounded-full vc-items-center vc-justify-center vc-flex vc-transition-all hover:vc-scale-110' +export function buttonDefaultStyle (on: boolean) { + return on + ? btnStyle + ' vc-text-primary' + : btnStyle + ' vc-text-gray-medium' +} -export function createButton ( - enabled: () => boolean, - tip: string, - action: () => void, - icon: ({ height, width, fill, className }) => JSX.Element, - isOn?: () => boolean -) { - return createAnyButton(enabled, tip, action, icon, isOn) +export function buttonBlueStyle (on: boolean) { + return btnStyle + ' vc-text-white' } -export function createBlueButton ( - enabled: () => boolean, - tip: string, - action: () => void, - icon: ({ height, width, fill, className }) => JSX.Element, + +export interface IControlBarButtonItem { + id: string, + enabled?: (() => boolean) | undefined + tip: string + action: () => void + icon: ({ height, width, fill, className }) => JSX.Element isOn?: () => boolean -) { - return createAnyButton(enabled, tip, action, icon, isOn, { on: 'vc-text-white', off: 'vc-text-white' }) + style: (on: boolean) => string } -function createAnyButton ( - enabled: () => boolean, - tip: string, - action: () => void, - icon: ({ height, width, fill, className }) => JSX.Element, - isOn: () => boolean, - colors: { on: string, off: string } = { on: 'vc-text-primary', off: 'vc-text-gray-medium' } -) { - if (!enabled()) return null - const style = isOn?.() - ? btnStyle + ' ' + colors.on - : btnStyle + ' ' + colors.off +export function createButton (button: IControlBarButtonItem) { + if (button.enabled !== undefined && !button.enabled()) return null + const style = button.style(button.isOn?.()) return ( - ) } diff --git a/src/controlbar/controlBarCamera.tsx b/src/controlbar/controlBarCamera.tsx deleted file mode 100644 index 99531b61..00000000 --- a/src/controlbar/controlBarCamera.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @module viw-webgl-component - */ - -import { useEffect, useState } from 'react' -import * as VIM from 'vim-webgl-viewer/' -import { ComponentCamera } from '../helpers/camera' -import * as Icons from '../panels/icons' -import { - ComponentSettings, - isTrue -} from '../settings/settings' -import { createButton } from './controlBarButton' -import { createSection } from './controlBarSection' - -export function TabCamera (props: {viewer: VIM.Viewer, camera: ComponentCamera; settings: ComponentSettings }) { - const viewer = props.viewer - const [mode, setMode] = useState(viewer.inputs.pointerActive) - - useEffect(() => { - const subPointer = viewer.inputs.onPointerModeChanged.subscribe(() => { - setMode(viewer.inputs.pointerActive) - }) - - // Clean up - return () => { - subPointer() - } - }, []) - - const onModeBtn = (target: VIM.PointerMode) => { - const next = mode === target ? viewer.inputs.pointerFallback : target - viewer.inputs.pointerActive = next - setMode(next) - } - - const btnOrbit = createButton( - () => isTrue(props.settings.ui.orbit), - 'Orbit', - () => onModeBtn('orbit'), - Icons.orbit, - () => mode === 'orbit' - ) - const btnLook = createButton( - () => isTrue(props.settings.ui.lookAround), - 'Look Around', - () => onModeBtn('look'), - Icons.look, - () => mode === 'look' - ) - const btnPan = createButton( - () => isTrue(props.settings.ui.pan), - 'Pan', - () => onModeBtn('pan'), - Icons.pan, - () => mode === 'pan' - ) - const btnZoom = createButton( - () => isTrue(props.settings.ui.zoom), - 'Zoom', - () => onModeBtn('zoom'), - Icons.zoom, - () => mode === 'zoom' - ) - const btnFrameRect = createButton( - () => isTrue(props.settings.ui.zoomWindow), - 'Zoom Window', - () => { - onModeBtn('rect') - viewer.gizmos.section.visible = false - viewer.gizmos.section.interactive = false - }, - Icons.frameRect, - () => mode === 'rect' - ) - const btnFrame = createButton( - () => isTrue(props.settings.ui.zoomToFit), - 'Zoom to Fit', - () => props.camera.frameContext(), - Icons.frameSelection, - () => false - ) - - return createSection('white', [btnOrbit, btnLook, btnPan, btnZoom, btnFrameRect, btnFrame]) -} diff --git a/src/controlbar/controlBarCommands.ts b/src/controlbar/controlBarCommands.ts new file mode 100644 index 00000000..0a0cd357 --- /dev/null +++ b/src/controlbar/controlBarCommands.ts @@ -0,0 +1,23 @@ +import ReactTooltip from 'react-tooltip' +import * as VIM from 'vim-webgl-viewer' + +export function getControlBarCommands (viewer: VIM.Viewer) { + const section = viewer.gizmos.section + + return { + toggleSectionBox: (viewer: VIM.Viewer) => { + ReactTooltip.hide() + + if (viewer.inputs.pointerActive === 'rect') { + viewer.inputs.pointerActive = viewer.inputs.pointerFallback + } + + const next = !( + section.visible && section.interactive + ) + + section.interactive = next + section.visible = next + } + } +} diff --git a/src/controlbar/controlBarSection.tsx b/src/controlbar/controlBarSection.tsx index f4ea955b..06544138 100644 --- a/src/controlbar/controlBarSection.tsx +++ b/src/controlbar/controlBarSection.tsx @@ -1,10 +1,20 @@ import React from 'react' +import { createButton, IControlBarButtonItem } from './controlBarButton' -export function createSection (theme: 'white' | 'blue', elements : (JSX.Element | null)[]) { - const bg = theme === 'white' ? 'vc-bg-white' : 'vc-bg-primary' - const style = 'vc-flex vc-items-center vc-rounded-full vc-mb-2 vc-px-2 vc-shadow-md' +const sectionStyle = 'vc-flex vc-items-center vc-rounded-full vc-mb-2 vc-px-2 vc-shadow-md' +export const sectionDefaultStyle = sectionStyle + ' vc-bg-white' +export const sectionBlueStyle = sectionStyle + ' vc-bg-primary' - return
- {...elements} +export interface IControlBarSection { + id: string, + enable? : (() => boolean) | undefined + buttons: IControlBarButtonItem[] + style: string +} + +export function createSection (section: IControlBarSection) { + if (section.enable !== undefined && !section.enable()) return null + return
+ {section.buttons.map(b => createButton(b))}
} diff --git a/src/controlbar/controlBarSettings.tsx b/src/controlbar/controlBarSettings.tsx deleted file mode 100644 index 9ee81fa8..00000000 --- a/src/controlbar/controlBarSettings.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @module viw-webgl-component - */ - -import { useEffect, useRef, useState } from 'react' -import { SideState } from '../sidePanel/sideState' -import * as Icons from '../panels/icons' -import { HelpState } from '../panels/help' -import { - ComponentSettings, isTrue -} from '../settings/settings' -import { createButton } from './controlBarButton' -import { createSection } from './controlBarSection' -import { FullScreenObserver } from '../helpers/fullScreenObserver' - -export function TabSettings (props: { - help: HelpState - side: SideState - settings: ComponentSettings -}) { - const fullScreenRef = useRef(new FullScreenObserver()) - const [fullScreen, setFullScreen] = useState( - fullScreenRef.current.isFullScreen() - ) - - useEffect(() => { - fullScreenRef.current.onFullScreenChange = (value) => setFullScreen(value) - return () => fullScreenRef.current.dispose() - }, []) - - const onHelpBtn = () => { - props.help.setVisible(!props.help.visible) - } - - const onTreeViewBtn = () => { - props.side.toggleContent('bim') - } - - const onSettingsBtn = () => { - props.side.toggleContent('settings') - } - - const btnTreeView = createButton( - () => isTrue(props.settings.ui.projectInspector), - 'Project Inspector', - onTreeViewBtn, - Icons.treeView, - () => props.side.getContent() === 'bim' - ) - const btnSettings = createButton( - () => isTrue(props.settings.ui.settings), - 'Settings', - onSettingsBtn, - Icons.settings, - () => props.side.getContent() === 'settings' - ) - - const btnHelp = createButton( - () => isTrue(props.settings.ui.help), - 'Help', - onHelpBtn, - Icons.help, - () => props.help.visible - ) - - const btnFullScreen = createButton( - () => - isTrue(props.settings.ui.maximise) && - props.settings.capacity.canGoFullScreen, - fullScreen ? 'Fullscreen' : 'Minimize', - () => { - if (fullScreen) { - document.exitFullscreen() - } else { - document.body.requestFullscreen() - } - }, - fullScreen ? Icons.minimize : Icons.fullsScreen - ) - - const tree = isTrue(props.settings.ui.bimTreePanel) || - isTrue(props.settings.ui.bimInfoPanel) - ? btnTreeView - : null - - return createSection('white', [tree, btnSettings, btnHelp, btnFullScreen]) -} diff --git a/src/controlbar/controlBarTools.tsx b/src/controlbar/controlBarTools.tsx deleted file mode 100644 index 45ec5227..00000000 --- a/src/controlbar/controlBarTools.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/** - * @module viw-webgl-component - */ - -import { useEffect, useRef, useState } from 'react' -import ReactTooltip from 'react-tooltip' -import * as VIM from 'vim-webgl-viewer/' -import { Isolation } from '../helpers/isolation' -import { CursorManager } from '../helpers/cursor' -import * as Icons from '../panels/icons' -import { - ComponentSettings, isTrue -} from '../settings/settings' -import { loopMeasure } from '../helpers/measureLoop' -import { createBlueButton, createButton } from './controlBarButton' -import { createSection } from './controlBarSection' - -/* TAB TOOLS */ -export function TabTools (props: { - viewer: VIM.Viewer - cursor: CursorManager - isolation: Isolation - settings: ComponentSettings -}) { - const viewer = props.viewer - // Need a ref to get the up to date value in callback. - const [measuring, setMeasuring] = useState(false) - const firstSection = useRef(true) - // eslint-disable-next-line no-unused-vars - const [measurement, setMeasurement] = useState() - const [section, setSection] = useState<{ clip: boolean; active: boolean }>({ - clip: viewer.gizmos.section.clip, - active: viewer.gizmos.section.visible && viewer.gizmos.section.interactive - }) - - const measuringRef = useRef() - measuringRef.current = measuring - - useEffect(() => { - const subSection = viewer.gizmos.section.onStateChanged.subscribe(() => - setSection({ - clip: viewer.gizmos.section.clip, - active: - viewer.gizmos.section.visible && viewer.gizmos.section.interactive - }) - ) - - // Clean up - return () => { - subSection() - } - }, []) - - const onSectionBtn = () => { - ReactTooltip.hide() - toggleSectionBox(viewer, firstSection.current) - firstSection.current = false - } - - const onMeasureBtn = () => { - ReactTooltip.hide() - - if (measuring) { - viewer.gizmos.measure.abort() - setMeasuring(false) - } else { - setMeasuring(true) - loopMeasure( - viewer, - () => measuringRef.current, - (m) => setMeasurement(m), - props.cursor.setCursor - ) - } - } - - const onResetSectionBtn = () => { - viewer.gizmos.section.fitBox(viewer.renderer.getBoundingBox()) - } - - const onSectionClip = () => { - viewer.gizmos.section.clip = true - } - const onSectionNoClip = () => { - viewer.gizmos.section.clip = false - } - - const onMeasureDeleteBtn = () => { - ReactTooltip.hide() - viewer.gizmos.measure.abort() - onMeasureBtn() - } - - const btnSection = createButton( - () => isTrue(props.settings.ui.sectioningMode), - 'Sectioning Mode', - onSectionBtn, - Icons.sectionBox - ) - - const btnMeasure = createButton( - () => isTrue(props.settings.ui.measuringMode), - 'Measuring Mode', - onMeasureBtn, - Icons.measure - ) - - const btnIsolation = createButton( - () => isTrue(props.settings.ui.toggleIsolation), - 'Toggle Isolation', - () => { - props.isolation.toggleIsolation('controlBar') - }, - Icons.toggleIsolation - ) - - const toolsTab = createSection('white', [btnSection, btnMeasure, btnIsolation]) - - const btnMeasureDelete = createBlueButton( - () => true, - 'Delete', - onMeasureDeleteBtn, - Icons.trash - ) - const btnMeasureConfirm = createBlueButton( - () => true, - 'Done', - onMeasureBtn, - Icons.checkmark - ) - const measureTab = createSection('blue', [btnMeasureDelete, btnMeasureConfirm]) - - const btnSectionReset = createBlueButton( - () => true, - 'Reset Section Box', - onResetSectionBtn, - Icons.sectionBoxReset - ) - const btnSectionShrink = createBlueButton( - () => true, - 'Shrink to Selection', - () => viewer.gizmos.section.fitBox(viewer.selection.getBoundingBox()), - Icons.sectionBoxShrink - ) - - const btnSectionClip = createBlueButton( - () => true, - 'Apply Section Box', - onSectionClip, - Icons.sectionBoxNoClip - ) - const btnSectionNoClip = createBlueButton( - () => true, - 'Ignore Section Box', - onSectionNoClip, - Icons.sectionBoxClip - ) - const btnSectionConfirm = createBlueButton( - () => true, - 'Done', - onSectionBtn, - Icons.checkmark - ) - - const sectionTab = createSection('blue', [ - btnSectionReset, - btnSectionShrink, - section.clip ? btnSectionNoClip : btnSectionClip, - btnSectionConfirm - ]) - - // There is a weird bug with tooltips not working properly - // if measureTab or sectionTab do not have the same number of buttons as toolstab - - return measuring ? measureTab : section.active ? sectionTab : toolsTab -} - -function toggleSectionBox (viewer: VIM.Viewer, isFirst: boolean) { - if (viewer.inputs.pointerActive === 'rect') { - viewer.inputs.pointerActive = viewer.inputs.pointerFallback - } - - const next = !( - viewer.gizmos.section.visible && viewer.gizmos.section.interactive - ) - - if (next) { - if (isFirst) { - viewer.gizmos.section.clip = true - viewer.gizmos.section.fitBox(viewer.renderer.getBoundingBox().expandByScalar(1)) - } - if (viewer.gizmos.section.box.containsPoint(viewer.camera.position)) { - viewer.camera.lerp(1).frame(viewer.renderer.section.box) - } - } - - viewer.gizmos.section.interactive = next - viewer.gizmos.section.visible = next -} diff --git a/src/controlbar/fullScreenState.ts b/src/controlbar/fullScreenState.ts new file mode 100644 index 00000000..37338204 --- /dev/null +++ b/src/controlbar/fullScreenState.ts @@ -0,0 +1,25 @@ +import { useEffect, useState, useRef } from 'react' +import { FullScreenObserver } from '../helpers/fullScreenObserver' + +export function getFullScreenState () { + const fullScreenRef = useRef(new FullScreenObserver()) + const isFullScren = () => fullScreenRef.current.isFullScreen() + const [, setFullScreen] = useState(isFullScren()) + useEffect(() => { + fullScreenRef.current.onFullScreenChange = (value) => setFullScreen(value) + + // Clean up + return () => fullScreenRef.current.dispose() + }, []) + + return { + get: () => isFullScren(), + toggle: () => { + if (isFullScren()) { + document.exitFullscreen() + } else { + document.body.requestFullscreen() + } + } + } +} diff --git a/src/controlbar/measureState.tsx b/src/controlbar/measureState.tsx new file mode 100644 index 00000000..b12b195b --- /dev/null +++ b/src/controlbar/measureState.tsx @@ -0,0 +1,65 @@ +import ReactTooltip from 'react-tooltip' +import { useRef, useState } from 'react' +import * as VIM from 'vim-webgl-viewer/' +import { CursorManager, pointerToCursor } from '../helpers/cursor' + +export function getMeasureState (viewer: VIM.Viewer, cursor: CursorManager) { + const measuringRef = useRef(false) + const activeRef = useRef(false) + const [active, setActive] = useState(measuringRef.current) + const [, setMeasurement] = useState() + + const toggle = () => { + ReactTooltip.hide() + + if (activeRef.current) { + viewer.gizmos.measure.abort() + activeRef.current = false + setActive(false) + } else { + activeRef.current = true + setActive(true) + loop() + } + } + + const clear = () => { + ReactTooltip.hide() + viewer.gizmos.measure.abort() + toggle() + } + + /** + * Behaviour to have measure gizmo loop over and over. + */ + const loop = () => { + const onMouseMove = () => { + setMeasurement(viewer.gizmos.measure.measurement) + } + cursor.setCursor('cursor-measure') + viewer.viewport.canvas.addEventListener('mousemove', onMouseMove) + viewer.gizmos.measure + .start() + .then(() => { + setMeasurement(viewer.gizmos.measure.measurement) + }) + .catch(() => { + setMeasurement(undefined) + }) + .finally(() => { + cursor.setCursor(pointerToCursor(viewer.inputs.pointerActive)) + viewer.viewport.canvas.removeEventListener('mousemove', onMouseMove) + if (activeRef.current) { + loop() + } else { + viewer.gizmos.measure.clear() + } + }) + } + + return { + active, + toggle, + clear + } +} diff --git a/src/controlbar/pointerState.ts b/src/controlbar/pointerState.ts new file mode 100644 index 00000000..f266b833 --- /dev/null +++ b/src/controlbar/pointerState.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react' +import * as VIM from 'vim-webgl-viewer/' + +export function getPointerState (viewer: VIM.Viewer) { + const [mode, setMode] = useState(viewer.inputs.pointerActive) + + useEffect(() => { + const sub = viewer.inputs.onPointerModeChanged.subscribe(() => { + setMode(viewer.inputs.pointerActive) + }) + return () => sub() + }, []) + + const onModeBtn = (target: VIM.PointerMode) => { + const next = mode === target ? viewer.inputs.pointerFallback : target + viewer.inputs.pointerActive = next + setMode(next) + } + + return { + mode, + setMode, + onButton: onModeBtn + } +} diff --git a/src/controlbar/restOfScreen.tsx b/src/controlbar/restOfScreen.tsx new file mode 100644 index 00000000..14a861da --- /dev/null +++ b/src/controlbar/restOfScreen.tsx @@ -0,0 +1,36 @@ + +import React, { useEffect, useRef, useState } from 'react' +import ReactTooltip from 'react-tooltip' +import { SideState } from '../sidePanel/sideState' + +export function RestOfScreen (props:{ + side: SideState, + content: () => JSX.Element +}) { + const [, setVersion] = useState(0) + const resizeObserver = useRef() + + // On Each Render + useEffect(() => { + ReactTooltip.rebuild() + }) + + useEffect(() => { + resizeObserver.current = new ResizeObserver(() => { + setVersion((prev) => prev ^ 1) + }) + resizeObserver.current.observe(document.body) + + return () => { + resizeObserver.current?.disconnect() + } + }, []) + + return ( +
+ {props.content()} +
) +} diff --git a/src/controlbar/sectionBoxState.ts b/src/controlbar/sectionBoxState.ts new file mode 100644 index 00000000..806f4071 --- /dev/null +++ b/src/controlbar/sectionBoxState.ts @@ -0,0 +1,60 @@ +import { useEffect, useState } from 'react' +import ReactTooltip from 'react-tooltip' +import * as VIM from 'vim-webgl-viewer/' + +export type SectionState = { + clip: boolean + active: boolean +} + +export function getSectionBoxState (viewer: VIM.Viewer) { + const sectionGizmo = viewer.gizmos.section + + const [section, setSection] = useState({ + clip: sectionGizmo.clip, + active: sectionGizmo.visible && sectionGizmo.interactive + }) + + // On First Render + useEffect(() => { + sectionGizmo.clip = true + const subSection = sectionGizmo.onStateChanged.subscribe(() => + setSection({ + clip: sectionGizmo.clip, + active: sectionGizmo.visible && sectionGizmo.interactive + }) + ) + + // Clean up + return () => subSection() + }, []) + + const toggle = () => { + ReactTooltip.hide() + + if (viewer.inputs.pointerActive === 'rect') { + viewer.inputs.pointerActive = viewer.inputs.pointerFallback + } + + const next = !( + sectionGizmo.visible && sectionGizmo.interactive + ) + + sectionGizmo.interactive = next + sectionGizmo.visible = next + } + + return { + clip: section.clip, + active: section.active, + set: setSection, + toggle, + hide: () => { + sectionGizmo.visible = false + sectionGizmo.interactive = false + }, + reset: () => sectionGizmo.fitBox(viewer.renderer.getBoundingBox()), + shrinkToSelection: () => sectionGizmo.fitBox(viewer.selection.getBoundingBox()), + toggleClip: () => { sectionGizmo.clip = (!section.clip) } + } +} diff --git a/src/helpers/deferredPromise.ts b/src/helpers/deferredPromise.ts new file mode 100644 index 00000000..9b33eed6 --- /dev/null +++ b/src/helpers/deferredPromise.ts @@ -0,0 +1,29 @@ +export class DeferredPromise extends Promise { + resolve: (value: T | PromiseLike) => void + reject: (reason: T | Error) => void + + initialCallStack: Error['stack'] + + constructor (executor: ConstructorParameters>[0] = () => {}) { + let resolver: (value: T | PromiseLike) => void + let rejector: (reason: T | Error) => void + + super((resolve, reject) => { + resolver = resolve + rejector = reject + return executor(resolve, reject) // Promise magic: this line is unexplicably essential + }) + + this.resolve = resolver! + this.reject = rejector! + + // store call stack for location where instance is created + this.initialCallStack = Error().stack?.split('\n').slice(2).join('\n') + } + + /** @throws error with amended call stack */ + rejectWithError (error: Error) { + error.stack = [error.stack?.split('\n')[0], this.initialCallStack].join('\n') + this.reject(error) + } +} diff --git a/src/helpers/measureLoop.ts b/src/helpers/measureLoop.ts deleted file mode 100644 index 25ade37a..00000000 --- a/src/helpers/measureLoop.ts +++ /dev/null @@ -1,36 +0,0 @@ - -import * as VIM from 'vim-webgl-viewer/' -import { Cursor, pointerToCursor } from './cursor' - -/** - * Behaviour to have measure gizmo loop over and over. - */ -export function loopMeasure ( - viewer: VIM.Viewer, - getMeasuring: () => boolean, - setMeasure: (value: VIM.THREE.Vector3) => void, - setCursor: (cursor: Cursor) => void -) { - const onMouseMove = () => { - setMeasure(viewer.gizmos.measure.measurement) - } - setCursor('cursor-measure') - viewer.viewport.canvas.addEventListener('mousemove', onMouseMove) - viewer.gizmos.measure - .start() - .then(() => { - setMeasure(viewer.gizmos.measure.measurement) - }) - .catch(() => { - setMeasure(undefined) - }) - .finally(() => { - setCursor(pointerToCursor(viewer.inputs.pointerActive)) - viewer.viewport.canvas.removeEventListener('mousemove', onMouseMove) - if (getMeasuring()) { - loopMeasure(viewer, getMeasuring, setMeasure, setCursor) - } else { - viewer.gizmos.measure.clear() - } - }) -} diff --git a/src/main.tsx b/src/main.tsx index 4a849cc3..1e6da7cb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,8 @@ import { createVimComponent, - VimComponentRef, getLocalSettings, - THREE, - VIM, - createContainer + THREE } from './component' // Parse URL @@ -15,65 +12,23 @@ const url = params.has('vim') ? params.get('vim') : null -const parent = document.createElement('div') -document.body.appendChild(parent) - -parent.style.position = 'absolute' -parent.style.top = '0%' -parent.style.right = '0%' -parent.style.left = '0%' -parent.style.bottom = '0%' - -const container = createContainer(parent) -// const container = createContainer() -createVimComponent(loadVim, container, getLocalSettings(), VIM.getViewerSettingsFromUrl(window.location.search)) - -async function loadVim (cmp: VimComponentRef) { +demo() +async function demo () { + const cmp = await createVimComponent(undefined, getLocalSettings()) const time = Date.now() const vim = await cmp.loader.open( url ?? 'https://vim02.azureedge.net/samples/residence.v1.2.75.vim', + // url ?? 'https://vim02.azureedge.net/samples/skanska.vim', // url ?? 'https://vim02.azureedge.net/samples/residence.v1.2.75.vimx', { progressive: true, rotation: new THREE.Vector3(270, 0, 0) } ) - //cmp.message.show('Loading completed') - /* - const gen = run() - setInterval(() => { - gen.next() - cmp.viewer.viewport.ResizeToParent() - }, 50) - const inc = 0.25 - function * run () { - while (true) { - for (let i = 0; i < 40; i += 0.25) { - parent.style.right = i + '%' - parent.style.left = i + '%' - yield 0 - } - for (let i = 0; i < 40; i += 0.25) { - parent.style.top = i + '%' - parent.style.bottom = i + '%' - yield 0 - } - for (let i = 40; i > 0; i -= 0.25) { - parent.style.top = i + '%' - parent.style.bottom = i + '%' - yield 0 - } - for (let i = 40; i > 0; i -= 0.25) { - parent.style.right = i + '%' - parent.style.left = i + '%' - yield 0 - } - } - } -*/ console.log(`Loading completed in ${((Date.now() - time) / 1000).toFixed(2)} seconds`) globalThis.THREE = THREE globalThis.component = cmp + globalThis.vim = vim } diff --git a/src/panels/contextMenu.tsx b/src/panels/contextMenu.tsx index 443acefc..3caec030 100644 --- a/src/panels/contextMenu.tsx +++ b/src/panels/contextMenu.tsx @@ -61,7 +61,7 @@ export const contextMenuElementIds = { /** * Represents a button in the context menu. It can't be clicked triggering given action. */ -export type contextMenuButton = { +export interface IContextMenuButton { id: string label: string keyboard: string @@ -72,18 +72,19 @@ export type contextMenuButton = { /** * Represents a divider in the context menu. It can't be clicked. */ -export type contextMenuDivider = { +export interface IContextMenuDivider { id: string enabled: boolean } -export type contextMenuElement = contextMenuButton | contextMenuDivider + +export type ContextMenuElement = IContextMenuButton | IContextMenuDivider /** * A map function that changes the context menu. */ -export type contextMenuCustomization = ( - e: contextMenuElement[] -) => contextMenuElement[] +export type ContextMenuCustomization = ( + e: ContextMenuElement[] +) => ContextMenuElement[] /** * Memoized version of VimContextMenu. @@ -99,7 +100,7 @@ export function VimContextMenu (props: { help: HelpState isolation: Isolation selection: VIM.IObject[] - customization?: (e: contextMenuElement[]) => contextMenuElement[] + customization?: (e: ContextMenuElement[]) => ContextMenuElement[] treeRef: React.MutableRefObject }) { const viewer = props.viewer @@ -207,7 +208,7 @@ export function VimContextMenu (props: { viewer.gizmos.section.fitBox(viewer.selection.getBoundingBox()) } - const createButton = (button: contextMenuButton) => { + const createButton = (button: IContextMenuButton) => { if (!button.enabled) return null return ( ) } - const createDivider = (divider: contextMenuDivider) => { + const createDivider = (divider: IContextMenuDivider) => { return divider.enabled ? ( void + customize : (customization: ContextMenuCustomization) => void + } + +export type ControlBarRef = { + /** + * Defines a callback function to dynamically customize the control bar. + * @param customization The configuration object specifying the customization options for the control bar. + */ + customize : (customization: ControlBarCustomization) => void } /** @@ -86,6 +95,11 @@ export type VimComponentRef = { */ contextMenu : ContextMenuRef + /** + * Context menu API managing the content and behavior of the context menu. + */ + controlBar : ControlBarRef + /** * Settings API managing settings applied to the component. */ @@ -105,4 +119,6 @@ export type VimComponentRef = { * API To interact with the BIM info panel. */ bimInfo: BimInfoPanelRef + + dispose: () => void }