├── .eslintignore ├── .eslintrc.cjs ├── .eslintrc.json ├── .github └── workflows │ ├── build.yaml │ ├── publish-dev.yaml │ └── publish.yaml ├── .gitignore ├── .prettierrc.json ├── .vscode └── tasks.json ├── docs ├── api │ ├── assets │ │ ├── highlight.css │ │ ├── main.js │ │ ├── search.js │ │ └── style.css │ ├── classes │ │ ├── vim_loader.ColorAttribute.html │ │ ├── vim_loader.DummySubsetBuilder.html │ │ ├── vim_loader.ElementMapping.html │ │ ├── vim_loader.ElementMapping2.html │ │ ├── vim_loader.ElementNoMapping.html │ │ ├── vim_loader.G3dMeshCounts.html │ │ ├── vim_loader.G3dMeshOffsets.html │ │ ├── vim_loader.G3dSubset.html │ │ ├── vim_loader.Geometry.MergeBuffer.html │ │ ├── vim_loader.Geometry.MergeInfo.html │ │ ├── vim_loader.Geometry.MergeResult.html │ │ ├── vim_loader.GeometrySubmesh.html │ │ ├── vim_loader.InsertableGeometry.html │ │ ├── vim_loader.InsertableMesh.html │ │ ├── vim_loader.InsertableSubmesh.html │ │ ├── vim_loader.InstancedMesh.html │ │ ├── vim_loader.InstancedMeshFactory.html │ │ ├── vim_loader.InstancedSubmesh.html │ │ ├── vim_loader.LoadingSynchronizer.html │ │ ├── vim_loader.Mesh.html │ │ ├── vim_loader.MeshBuilder.html │ │ ├── vim_loader.Object.html │ │ ├── vim_loader.ObjectAttribute.html │ │ ├── vim_loader.Scene.html │ │ ├── vim_loader.SceneBuilder.html │ │ ├── vim_loader.StandardSubmesh.html │ │ ├── vim_loader.SubsetRequest.html │ │ ├── vim_loader.Vim.html │ │ ├── vim_loader.VimBuilder.html │ │ ├── vim_loader.VimMeshFactory.html │ │ ├── vim_loader.VimSubsetBuilder.html │ │ ├── vim_loader.Vimx.html │ │ ├── vim_loader.VimxSubsetBuilder.html │ │ ├── vim_loader_legacy_legacyLoader.LegacyLoader.html │ │ ├── vim_loader_legacy_vimRequest.VimRequest.html │ │ ├── vim_loader_materials.MergeMaterial.html │ │ ├── vim_loader_materials.OutlineMaterial.html │ │ ├── vim_loader_materials.StandardMaterial.html │ │ ├── vim_loader_materials.ViewerMaterials.html │ │ ├── vim_webgl_viewer_gizmos_gizmos.Gizmos.html │ │ ├── vim_webgl_viewer_gizmos_markers_gizmoMarker.GizmoMarker.html │ │ ├── vim_webgl_viewer_gizmos_markers_gizmoMarkers.GizmoMarkers.html │ │ ├── viw_webgl_viewer.Environment.html │ │ ├── viw_webgl_viewer.GroundPlane.html │ │ ├── viw_webgl_viewer.InputAction.html │ │ ├── viw_webgl_viewer.RaycastResult.html │ │ ├── viw_webgl_viewer.Raycaster.html │ │ ├── viw_webgl_viewer.Selection.html │ │ ├── viw_webgl_viewer.Viewer.html │ │ ├── viw_webgl_viewer.Viewport.html │ │ ├── viw_webgl_viewer_camera.Camera.html │ │ ├── viw_webgl_viewer_camera.CameraLerp.html │ │ ├── viw_webgl_viewer_camera.CameraMovement.html │ │ ├── viw_webgl_viewer_camera.CameraMovementSnap.html │ │ ├── viw_webgl_viewer_camera.OrthographicWrapper.html │ │ ├── viw_webgl_viewer_camera.PerspectiveWrapper.html │ │ ├── viw_webgl_viewer_gizmos.Axis.html │ │ ├── viw_webgl_viewer_gizmos.GizmoAxes.html │ │ ├── viw_webgl_viewer_gizmos.GizmoOptions.html │ │ ├── viw_webgl_viewer_gizmos.GizmoOrbit.html │ │ ├── viw_webgl_viewer_gizmos.GizmoRectangle.html │ │ ├── viw_webgl_viewer_gizmos_measure.Measure.html │ │ ├── viw_webgl_viewer_gizmos_measure.MeasureFlow.html │ │ ├── viw_webgl_viewer_gizmos_measure.MeasureGizmo.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.BoxHighlight.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.BoxInputs.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.BoxMesh.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.BoxOutline.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.GizmoLoading.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.SectionBox.html │ │ ├── viw_webgl_viewer_inputs.DefaultInputScheme.html │ │ ├── viw_webgl_viewer_inputs.Input.html │ │ ├── viw_webgl_viewer_inputs.InputHandler.html │ │ ├── viw_webgl_viewer_inputs.KeyboardHandler.html │ │ ├── viw_webgl_viewer_inputs.MouseHandler.html │ │ ├── viw_webgl_viewer_inputs.TouchHandler.html │ │ ├── viw_webgl_viewer_rendering.MergePass.html │ │ ├── viw_webgl_viewer_rendering.OutlinePass.html │ │ ├── viw_webgl_viewer_rendering.RenderScene.html │ │ ├── viw_webgl_viewer_rendering.Renderer.html │ │ ├── viw_webgl_viewer_rendering.RenderingComposer.html │ │ ├── viw_webgl_viewer_rendering.RenderingSection.html │ │ └── viw_webgl_viewer_rendering.TransferPass.html │ ├── functions │ │ ├── vim_loader.Geometry.createGeometryFromArrays.html │ │ ├── vim_loader.Geometry.createGeometryFromInstances.html │ │ ├── vim_loader.Geometry.createGeometryFromMesh.html │ │ ├── vim_loader.Geometry.getInstanceMatrix.html │ │ ├── vim_loader.Geometry.mergeInstanceMeshes.html │ │ ├── vim_loader.Geometry.mergeUniqueMeshes.html │ │ ├── vim_loader.Transparency.isValid.html │ │ ├── vim_loader.Transparency.requiresAlpha.html │ │ ├── vim_loader.getFullSettings.html │ │ ├── vim_loader_materials.createIsolationMaterial.html │ │ ├── vim_loader_materials.createMaskMaterial.html │ │ ├── vim_loader_materials.createMergeMaterial.html │ │ ├── vim_loader_materials.createOpaque.html │ │ ├── vim_loader_materials.createOutlineMaterial.html │ │ ├── vim_loader_materials.createTransferMaterial.html │ │ ├── vim_loader_materials.createTransparent.html │ │ ├── vim_loader_materials.createWireframe.html │ │ ├── vim_loader_progressive_open.open.html │ │ ├── viw_webgl_viewer.getSettings.html │ │ └── viw_webgl_viewer_gizmos_measure.createMeasureElement.html │ ├── index.html │ ├── interfaces │ │ ├── vim_loader.IObject.html │ │ ├── vim_loader.IRenderer.html │ │ ├── vim_loader.SubsetBuilder.html │ │ ├── viw_webgl_viewer.IEnvironment.html │ │ ├── viw_webgl_viewer_camera.ICamera.html │ │ ├── viw_webgl_viewer_gizmos_measure.IMeasure.html │ │ └── viw_webgl_viewer_inputs.InputScheme.html │ ├── modules.html │ ├── modules │ │ ├── vim_loader.Geometry.html │ │ ├── vim_loader.Transparency.html │ │ ├── vim_loader.html │ │ ├── vim_loader_legacy_legacyLoader.html │ │ ├── vim_loader_legacy_vimRequest.html │ │ ├── vim_loader_materials.html │ │ ├── vim_loader_progressive_open.html │ │ ├── vim_webgl_viewer_gizmos_gizmos.html │ │ ├── vim_webgl_viewer_gizmos_markers_gizmoMarker.html │ │ ├── vim_webgl_viewer_gizmos_markers_gizmoMarkers.html │ │ ├── viw_webgl_viewer.html │ │ ├── viw_webgl_viewer_camera.html │ │ ├── viw_webgl_viewer_gizmos.html │ │ ├── viw_webgl_viewer_gizmos_measure.html │ │ ├── viw_webgl_viewer_gizmos_sectionBox.html │ │ ├── viw_webgl_viewer_inputs.html │ │ └── viw_webgl_viewer_rendering.html │ ├── types │ │ ├── vim_loader.FileType.html │ │ ├── vim_loader.InstancingArgs.html │ │ ├── vim_loader.LoadPartialSettings.html │ │ ├── vim_loader.LoadSettings.html │ │ ├── vim_loader.MergeArgs.html │ │ ├── vim_loader.MergedSubmesh.html │ │ ├── vim_loader.ObjectType.html │ │ ├── vim_loader.Submesh.html │ │ ├── vim_loader.Transparency.Mode.html │ │ ├── vim_loader.VimPartialSettings.html │ │ ├── vim_loader.VimSettings.html │ │ ├── vim_loader_materials.ShaderUniforms.html │ │ ├── viw_webgl_viewer.ActionModifier.html │ │ ├── viw_webgl_viewer.ActionType.html │ │ ├── viw_webgl_viewer.PartialSettings.html │ │ ├── viw_webgl_viewer.RecursivePartial.html │ │ ├── viw_webgl_viewer.Settings.html │ │ ├── viw_webgl_viewer.TextureEncoding.html │ │ ├── viw_webgl_viewer.ThreeIntersectionList.html │ │ ├── viw_webgl_viewer_gizmos_measure.MeasureElement.html │ │ ├── viw_webgl_viewer_gizmos_measure.MeasureStage.html │ │ ├── viw_webgl_viewer_gizmos_measure.MeasureStyle.html │ │ └── viw_webgl_viewer_inputs.PointerMode.html │ └── variables │ │ ├── vim_loader.defaultConfig.html │ │ ├── viw_webgl_viewer.defaultViewerSettings.html │ │ └── viw_webgl_viewer_inputs.KEYS-1.html ├── assets │ ├── favicon.ico │ └── logo.png ├── index-dev.html ├── index.html └── ultra.html ├── index.html ├── license.txt ├── package.json ├── readme.md ├── residence.vimx ├── spanish.vim ├── src ├── images.ts ├── index.ts ├── main.ts ├── style.css ├── utils │ ├── boxes.ts │ ├── deferredPromise.ts │ ├── meshLine.js │ └── requestResult.ts ├── vim-loader │ ├── averageBoundingBox.ts │ ├── colorAttributes.ts │ ├── elementMapping.ts │ ├── geometry.ts │ ├── materials │ │ ├── isolationMaterial.ts │ │ ├── maskMaterial.ts │ │ ├── mergeMaterial.ts │ │ ├── outlineMaterial.ts │ │ ├── simpleMaterial.ts │ │ ├── skyboxMaterial.ts │ │ ├── standardMaterial.ts │ │ ├── transferMaterial.ts │ │ └── viewerMaterials.ts │ ├── mesh.ts │ ├── object3D.ts │ ├── objectAttributes.ts │ ├── progressive │ │ ├── g3dOffsets.ts │ │ ├── g3dSubset.ts │ │ ├── insertableGeometry.ts │ │ ├── insertableMesh.ts │ │ ├── insertableSubmesh.ts │ │ ├── instancedMesh.ts │ │ ├── instancedMeshFactory.ts │ │ ├── instancedSubmesh.ts │ │ ├── legacyMeshFactory.ts │ │ ├── loadingSynchronizer.ts │ │ ├── open.ts │ │ ├── subsetBuilder.ts │ │ ├── subsetRequest.ts │ │ ├── vimRequest.ts │ │ └── vimx.ts │ ├── scene.ts │ ├── vim.ts │ └── vimSettings.ts └── vim-webgl-viewer │ ├── camera │ ├── camera.ts │ ├── cameraMovement.ts │ ├── cameraMovementLerp.ts │ ├── cameraMovementSnap.ts │ ├── orthographic.ts │ └── perspective.ts │ ├── environment │ ├── cameraLight.ts │ ├── environment.ts │ ├── groundPlane.ts │ └── skybox.ts │ ├── gizmos │ ├── axes │ │ ├── axes.ts │ │ ├── axesSettings.ts │ │ └── gizmoAxes.ts │ ├── gizmoLoading.ts │ ├── gizmoOrbit.ts │ ├── gizmoRectangle.ts │ ├── gizmos.ts │ ├── markers │ │ ├── gizmoMarker.ts │ │ └── gizmoMarkers.ts │ ├── measure │ │ ├── measure.ts │ │ ├── measureFlow.ts │ │ ├── measureGizmo.ts │ │ └── measureHtml.ts │ └── sectionBox │ │ ├── sectionBox.ts │ │ ├── sectionBoxGizmo.ts │ │ └── sectionBoxInputs.ts │ ├── inputs │ ├── input.ts │ ├── inputHandler.ts │ ├── keyboard.ts │ ├── mouse.ts │ └── touch.ts │ ├── raycaster.ts │ ├── rendering │ ├── mergePass.ts │ ├── outlinePass.ts │ ├── renderScene.ts │ ├── renderer.ts │ ├── renderingComposer.ts │ ├── renderingSection.ts │ └── transferPass.ts │ ├── selection.ts │ ├── settings │ ├── defaultViewerSettings.ts │ ├── viewerSettings.ts │ └── viewerSettingsParsing.ts │ ├── viewer.ts │ └── viewport.ts ├── tsconfig.json └── vite.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['standard'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | rules: {}, 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install dependencies 14 | run: npm install 15 | - name: Build 16 | run: npm run build -------------------------------------------------------------------------------- /.github/workflows/publish-dev.yaml: -------------------------------------------------------------------------------- 1 | name: Publish dev version to NPM on push to develop 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup user 15 | run: | 16 | git config --global user.email "simon.roberge@vimaec.com" 17 | git config --global user.name "Simon Roberge" 18 | 19 | - name: Pull changes from remote 20 | run: git pull origin develop 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: '14.x' 26 | registry-url: 'https://registry.npmjs.org' 27 | 28 | - name: Install dependencies 29 | run: npm install 30 | 31 | - name: Get current version 32 | run: echo "CURRENT_VERSION=$(npm version --json | jq -r '.version')" >> $GITHUB_ENV 33 | 34 | - name: Bump dev version 35 | run: echo "NEW_VERSION=$(npm --no-git-tag-version version prerelease --preid=dev | cut -c 2-)" >> $GITHUB_ENV 36 | 37 | - name: Commit version bump 38 | run: git commit -am "Bump version to $NEW_VERSION" 39 | 40 | - name: Push changes 41 | run: git push 42 | 43 | - name: Build 44 | run: npm run build 45 | 46 | - name: Publish dev package on NPM 📦 47 | run: npm publish --tag=dev 48 | env: 49 | NODE_AUTH_TOKEN: ${{ secrets.VIM_NPM_PUSH }} 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish official release to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Setup Node 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '14.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Install dependencies 19 | run: npm install 20 | - name: Build 21 | run: npm run build 22 | - name: Publish package on NPM 📦 23 | run: npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.VIM_NPM_PUSH }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ignore config folders 2 | .* 3 | !.eslintignore 4 | !.eslintrc.cjs 5 | !.eslintrc.json 6 | !.prettierrc.json 7 | 8 | #ignore test files 9 | *.g3d 10 | *.vim 11 | *.gz 12 | 13 | 14 | #ignore build 15 | dist/* 16 | 17 | #ignore packages 18 | node_modules/* 19 | 20 | #ignore models 21 | models/* 22 | 23 | # This locks in development chain dependencies which is not appropriate. 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "option": "watch", 8 | "problemMatcher": [ 9 | "$tsc-watch" 10 | ], 11 | "group": "build", 12 | "label": "tsc: watch - tsconfig.json" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /docs/api/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #800000; 3 | --dark-hl-0: #808080; 4 | --light-hl-1: #800000; 5 | --dark-hl-1: #569CD6; 6 | --light-hl-2: #000000; 7 | --dark-hl-2: #D4D4D4; 8 | --light-hl-3: #000000FF; 9 | --dark-hl-3: #D4D4D4; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #800000; 13 | --dark-hl-5: #D7BA7D; 14 | --light-hl-6: #FF0000; 15 | --dark-hl-6: #9CDCFE; 16 | --light-hl-7: #098658; 17 | --dark-hl-7: #B5CEA8; 18 | --light-hl-8: #0451A5; 19 | --dark-hl-8: #CE9178; 20 | --light-hl-9: #0000FF; 21 | --dark-hl-9: #CE9178; 22 | --light-hl-10: #0000FF; 23 | --dark-hl-10: #569CD6; 24 | --light-hl-11: #795E26; 25 | --dark-hl-11: #DCDCAA; 26 | --light-hl-12: #0070C1; 27 | --dark-hl-12: #4FC1FF; 28 | --light-hl-13: #AF00DB; 29 | --dark-hl-13: #C586C0; 30 | --light-hl-14: #A31515; 31 | --dark-hl-14: #CE9178; 32 | --light-hl-15: #001080; 33 | --dark-hl-15: #9CDCFE; 34 | --light-code-background: #FFFFFF; 35 | --dark-code-background: #1E1E1E; 36 | } 37 | 38 | @media (prefers-color-scheme: light) { :root { 39 | --hl-0: var(--light-hl-0); 40 | --hl-1: var(--light-hl-1); 41 | --hl-2: var(--light-hl-2); 42 | --hl-3: var(--light-hl-3); 43 | --hl-4: var(--light-hl-4); 44 | --hl-5: var(--light-hl-5); 45 | --hl-6: var(--light-hl-6); 46 | --hl-7: var(--light-hl-7); 47 | --hl-8: var(--light-hl-8); 48 | --hl-9: var(--light-hl-9); 49 | --hl-10: var(--light-hl-10); 50 | --hl-11: var(--light-hl-11); 51 | --hl-12: var(--light-hl-12); 52 | --hl-13: var(--light-hl-13); 53 | --hl-14: var(--light-hl-14); 54 | --hl-15: var(--light-hl-15); 55 | --code-background: var(--light-code-background); 56 | } } 57 | 58 | @media (prefers-color-scheme: dark) { :root { 59 | --hl-0: var(--dark-hl-0); 60 | --hl-1: var(--dark-hl-1); 61 | --hl-2: var(--dark-hl-2); 62 | --hl-3: var(--dark-hl-3); 63 | --hl-4: var(--dark-hl-4); 64 | --hl-5: var(--dark-hl-5); 65 | --hl-6: var(--dark-hl-6); 66 | --hl-7: var(--dark-hl-7); 67 | --hl-8: var(--dark-hl-8); 68 | --hl-9: var(--dark-hl-9); 69 | --hl-10: var(--dark-hl-10); 70 | --hl-11: var(--dark-hl-11); 71 | --hl-12: var(--dark-hl-12); 72 | --hl-13: var(--dark-hl-13); 73 | --hl-14: var(--dark-hl-14); 74 | --hl-15: var(--dark-hl-15); 75 | --code-background: var(--dark-code-background); 76 | } } 77 | 78 | :root[data-theme='light'] { 79 | --hl-0: var(--light-hl-0); 80 | --hl-1: var(--light-hl-1); 81 | --hl-2: var(--light-hl-2); 82 | --hl-3: var(--light-hl-3); 83 | --hl-4: var(--light-hl-4); 84 | --hl-5: var(--light-hl-5); 85 | --hl-6: var(--light-hl-6); 86 | --hl-7: var(--light-hl-7); 87 | --hl-8: var(--light-hl-8); 88 | --hl-9: var(--light-hl-9); 89 | --hl-10: var(--light-hl-10); 90 | --hl-11: var(--light-hl-11); 91 | --hl-12: var(--light-hl-12); 92 | --hl-13: var(--light-hl-13); 93 | --hl-14: var(--light-hl-14); 94 | --hl-15: var(--light-hl-15); 95 | --code-background: var(--light-code-background); 96 | } 97 | 98 | :root[data-theme='dark'] { 99 | --hl-0: var(--dark-hl-0); 100 | --hl-1: var(--dark-hl-1); 101 | --hl-2: var(--dark-hl-2); 102 | --hl-3: var(--dark-hl-3); 103 | --hl-4: var(--dark-hl-4); 104 | --hl-5: var(--dark-hl-5); 105 | --hl-6: var(--dark-hl-6); 106 | --hl-7: var(--dark-hl-7); 107 | --hl-8: var(--dark-hl-8); 108 | --hl-9: var(--dark-hl-9); 109 | --hl-10: var(--dark-hl-10); 110 | --hl-11: var(--dark-hl-11); 111 | --hl-12: var(--dark-hl-12); 112 | --hl-13: var(--dark-hl-13); 113 | --hl-14: var(--dark-hl-14); 114 | --hl-15: var(--dark-hl-15); 115 | --code-background: var(--dark-code-background); 116 | } 117 | 118 | .hl-0 { color: var(--hl-0); } 119 | .hl-1 { color: var(--hl-1); } 120 | .hl-2 { color: var(--hl-2); } 121 | .hl-3 { color: var(--hl-3); } 122 | .hl-4 { color: var(--hl-4); } 123 | .hl-5 { color: var(--hl-5); } 124 | .hl-6 { color: var(--hl-6); } 125 | .hl-7 { color: var(--hl-7); } 126 | .hl-8 { color: var(--hl-8); } 127 | .hl-9 { color: var(--hl-9); } 128 | .hl-10 { color: var(--hl-10); } 129 | .hl-11 { color: var(--hl-11); } 130 | .hl-12 { color: var(--hl-12); } 131 | .hl-13 { color: var(--hl-13); } 132 | .hl-14 { color: var(--hl-14); } 133 | .hl-15 { color: var(--hl-15); } 134 | pre, code { background: var(--code-background); } 135 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vimaec/vim-webgl-viewer/d739b64d2e181d30e22c641fa889b9ae150960a8/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vimaec/vim-webgl-viewer/d739b64d2e181d30e22c641fa889b9ae150960a8/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/index-dev.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 21 | VIM 3D Model Viewer 22 | 26 | 27 | 28 | 29 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 21 | VIM 3D Model Viewer 22 | 26 | 27 | 28 | 29 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/ultra.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 21 | VIM 3D Model Viewer 22 | 26 | 27 | 28 |
29 | 30 | 33 | 34 | 35 | 36 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 23 | VIM Viewer 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 VIMaec LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vim-webgl-viewer", 3 | "version": "2.0.23", 4 | "description": "A high-performance 3D viewer and VIM file loader built on top of Three.JS.", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "./dist/vim-webgl-viewer.iife.js", 9 | "types": "./dist/types/index.d.ts", 10 | "module": "/dist/vim-webgl-viewer.mjs", 11 | "homepage": "https://github.com/vimaec/vim-webgl-viewer.git", 12 | "bugs": { 13 | "url": "https://github.com/vimaec/vim-webgl-viewer/issues" 14 | }, 15 | "license": "MIT", 16 | "author": "VIM ", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/vimaec/vim-webgl-viewer.git" 20 | }, 21 | "scripts": { 22 | "dev": "vite --host", 23 | "build": "vite build && npm run declarations", 24 | "package": "npm run build && npm publish", 25 | "serve-docs": "http-server ./docs -o --host", 26 | "eslint": "eslint --ext .js,.ts src --fix", 27 | "documentation": "typedoc --entryPointStrategy expand --mergeModulesMergeMode module --out docs/api --excludePrivate ./src/vim-webgl-viewer/ ./src/vim-loader/ && git add ./docs/", 28 | "declarations": "tsc --declaration --emitDeclarationOnly --outdir ./dist/types" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^18.11.11", 32 | "@typescript-eslint/eslint-plugin": "^5.45.1", 33 | "@typescript-eslint/parser": "^5.45.1", 34 | "eslint": "^8.29.0", 35 | "eslint-config-prettier": "^8.5.0", 36 | "eslint-config-standard": "^17.0.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "eslint-plugin-node": "^11.1.0", 39 | "eslint-plugin-prettier": "^4.2.1", 40 | "eslint-plugin-promise": "^6.1.1", 41 | "http-server": "^14", 42 | "opener": "^1.5.2", 43 | "prettier": "^2.8.0", 44 | "typedoc": "^0.23.21", 45 | "typedoc-plugin-merge-modules": "^4.0.1", 46 | "typescript": "^4.9.3", 47 | "vite": "^3.2.5" 48 | }, 49 | "bundleDependencies": [ 50 | "three" 51 | ], 52 | "dependencies": { 53 | "@types/three": "^0.143.0", 54 | "deepmerge": "^4.2.2", 55 | "is-plain-object": "^5.0.0", 56 | "ste-events": "^3.0.7", 57 | "ste-signals": "^3.0.9", 58 | "ste-simple-events": "^3.0.7", 59 | "three": "0.143.0", 60 | "vim-format": "1.0.14" 61 | }, 62 | "keywords": [ 63 | "3d", 64 | "viewer", 65 | "three.js", 66 | "model", 67 | "aec", 68 | "vim", 69 | "loader", 70 | "webgl" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This repository is being archived. Further developpement will take place at [vim-web](https://github.com/vimaec/vim-web) 2 | -------------------------------------------------------------------------------- /residence.vimx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vimaec/vim-webgl-viewer/d739b64d2e181d30e22c641fa889b9ae150960a8/residence.vimx -------------------------------------------------------------------------------- /spanish.vim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vimaec/vim-webgl-viewer/d739b64d2e181d30e22c641fa889b9ae150960a8/spanish.vim -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Links files to generate package type exports 2 | import './style.css' 3 | import { BFastSource } from 'vim-format' 4 | export * as THREE from 'three' 5 | 6 | export type VimSource = BFastSource 7 | export { IProgressLogs } from 'vim-format' 8 | export * from './vim-loader/progressive/open' 9 | export * from './vim-loader/progressive/vimRequest' 10 | export * from './vim-loader/progressive/vimx' 11 | export * from './vim-webgl-viewer/viewer' 12 | export * from './vim-loader/geometry' 13 | export type { PointerMode, InputScheme } from './vim-webgl-viewer/inputs/input' 14 | export { DefaultInputScheme, KEYS } from './vim-webgl-viewer/inputs/input' 15 | 16 | export * from './vim-webgl-viewer/settings/viewerSettings' 17 | export * from './vim-webgl-viewer/settings/viewerSettingsParsing' 18 | export * from './vim-webgl-viewer/settings/defaultViewerSettings' 19 | 20 | export { 21 | RaycastResult as HitTestResult, 22 | InputAction 23 | } from './vim-webgl-viewer/raycaster' 24 | 25 | export { type SelectableObject } from './vim-webgl-viewer/selection' 26 | export * from './vim-loader/progressive/insertableMesh' 27 | export * from './vim-loader/progressive/g3dSubset' 28 | export * from './vim-loader/geometry' 29 | export * from './vim-loader/materials/viewerMaterials' 30 | export * from './vim-loader/object3D' 31 | export * from './vim-loader/scene' 32 | export * from './vim-loader/vim' 33 | export * from './vim-loader/vimSettings' 34 | export * from './utils/boxes' 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Viewer, request, THREE, getViewerSettingsFromUrl } from '.' 2 | 3 | // Parse URL for source file 4 | const params = new URLSearchParams(window.location.search) 5 | const url = params.has('vim') 6 | ? params.get('vim') 7 | : null 8 | 9 | const viewer = new Viewer({ 10 | ...getViewerSettingsFromUrl(window.location.search) 11 | }) 12 | 13 | load(url ?? 'https://vim02.azureedge.net/samples/residence.v1.2.75.vim') 14 | addLoadButton() 15 | 16 | async function load (url: string | ArrayBuffer) { 17 | viewer.gizmos.loading.visible = true 18 | 19 | const r = request({ 20 | url: 'https://vimdevelopment01storage.blob.core.windows.net/samples/Wolford_Residence.r2025.vim' 21 | }, 22 | { 23 | rotation: new THREE.Vector3(270, 0, 0) 24 | }) 25 | 26 | for await (const progress of r.getProgress()) { 27 | console.log(`Downloading Vim (${(progress.loaded / 1000).toFixed(0)} kb)`) 28 | } 29 | 30 | const result = await r.getResult() 31 | viewer.gizmos.loading.visible = false 32 | if (result.isError()) { 33 | console.error(result.error) 34 | return 35 | } 36 | 37 | const vim = result.result 38 | 39 | await vim.loadAll() 40 | viewer.add(vim) 41 | viewer.camera.snap(true).frame(vim) 42 | viewer.camera.save() 43 | 44 | // Useful for debuging in console. 45 | globalThis.THREE = THREE 46 | globalThis.vim = vim 47 | globalThis.viewer = viewer 48 | } 49 | 50 | function addLoadButton () { 51 | const input = document.createElement('input') 52 | input.type = 'file' 53 | document.body.prepend(input) 54 | 55 | input.onchange = (e: any) => { 56 | viewer.clear() 57 | // getting a hold of the file reference 58 | const file = e.target.files[0] 59 | 60 | // setting up the reader 61 | const reader = new FileReader() 62 | reader.readAsArrayBuffer(file) 63 | 64 | // here we tell the reader what to do when it's done reading... 65 | reader.onload = (readerEvent) => { 66 | const content = readerEvent?.target?.result // this is the content! 67 | if (content) load(content) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .vim-measure { 2 | background-color: white; 3 | } 4 | 5 | .vim-measure table { 6 | border-collapse: collapse; 7 | } 8 | 9 | .vim-measure table tr { 10 | border: 1px solid; 11 | border-color: black; 12 | } 13 | 14 | .vim-measure-label-d, 15 | .vim-measure-label-x, 16 | .vim-measure-label-y, 17 | .vim-measure-label-z { 18 | color: white; 19 | width: 10px; 20 | text-align: center; 21 | padding: 5px; 22 | } 23 | 24 | .vim-measure-label-d { 25 | background-color: black; 26 | } 27 | .vim-measure-label-x { 28 | background-color: red; 29 | } 30 | .vim-measure-label-y { 31 | background-color: green; 32 | } 33 | .vim-measure-label-z { 34 | background-color: blue; 35 | } 36 | 37 | .vim-measure-value-d, 38 | .vim-measure-value-x, 39 | .vim-measure-value-y, 40 | .vim-measure-value-z { 41 | text-align: center; 42 | padding: 5px; 43 | } 44 | 45 | .lds-roller { 46 | display: block; 47 | top: 10px; 48 | left: 10px; 49 | position: absolute; 50 | width: 20px; 51 | height: 20px; 52 | } 53 | .lds-roller div { 54 | animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 55 | transform-origin: 20px 20px; 56 | } 57 | .lds-roller div:after { 58 | content: ' '; 59 | display: block; 60 | position: absolute; 61 | width: 4px; 62 | height: 4px; 63 | border-radius: 50%; 64 | background: #40a6de; 65 | margin: -4px 0 0 -4px; 66 | } 67 | .lds-roller div:nth-child(1) { 68 | animation-delay: -0.036s; 69 | } 70 | .lds-roller div:nth-child(1):after { 71 | top: 32px; 72 | left: 32px; 73 | } 74 | .lds-roller div:nth-child(2) { 75 | animation-delay: -0.072s; 76 | } 77 | .lds-roller div:nth-child(2):after { 78 | top: 35px; 79 | left: 28px; 80 | } 81 | .lds-roller div:nth-child(3) { 82 | animation-delay: -0.108s; 83 | } 84 | .lds-roller div:nth-child(3):after { 85 | top: 37px; 86 | left: 24px; 87 | } 88 | .lds-roller div:nth-child(4) { 89 | animation-delay: -0.144s; 90 | } 91 | .lds-roller div:nth-child(4):after { 92 | top: 38px; 93 | left: 20px; 94 | } 95 | .lds-roller div:nth-child(5) { 96 | animation-delay: -0.18s; 97 | } 98 | .lds-roller div:nth-child(5):after { 99 | top: 37px; 100 | left: 16px; 101 | } 102 | .lds-roller div:nth-child(6) { 103 | animation-delay: -0.216s; 104 | } 105 | .lds-roller div:nth-child(6):after { 106 | top: 35px; 107 | left: 12px; 108 | } 109 | .lds-roller div:nth-child(7) { 110 | animation-delay: -0.252s; 111 | } 112 | .lds-roller div:nth-child(7):after { 113 | top: 32px; 114 | left: 8px; 115 | } 116 | .lds-roller div:nth-child(8) { 117 | animation-delay: -0.288s; 118 | } 119 | .lds-roller div:nth-child(8):after { 120 | top: 28px; 121 | left: 5px; 122 | } 123 | @keyframes lds-roller { 124 | 0% { 125 | transform: rotate(0deg); 126 | } 127 | 100% { 128 | transform: rotate(360deg); 129 | } 130 | } 131 | 132 | .loader { 133 | width: 100%; 134 | height: 4px; 135 | display: block; 136 | top: 0px; 137 | position: absolute; 138 | overflow: hidden; 139 | } 140 | .loader::after { 141 | content: ''; 142 | width: 30%; 143 | height: 4.8px; 144 | background: linear-gradient( 145 | to right, 146 | #40a6de00 0%, 147 | #40a6deff 20%, 148 | #40a6deff 80%, 149 | #40a6de00 100% 150 | ); 151 | position: absolute; 152 | top: 0; 153 | left: 0; 154 | box-sizing: border-box; 155 | animation: animloader 2s linear infinite; 156 | } 157 | 158 | @keyframes animloader { 159 | 0% { 160 | left: 0; 161 | transform: translateX(-100%); 162 | } 163 | 100% { 164 | left: 100%; 165 | transform: translateX(0%); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/utils/boxes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module utils 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | export function createBoxes (boxes: THREE.Box3[]) { 8 | const center = new THREE.Vector3() 9 | const size = new THREE.Vector3() 10 | const quaternion = new THREE.Quaternion() 11 | 12 | const matrices = boxes.map((b) => { 13 | b.getCenter(center) 14 | b.getSize(size) 15 | return new THREE.Matrix4().compose(center, quaternion, size) 16 | }) 17 | 18 | const cube = new THREE.BoxBufferGeometry(1, 1, 1) 19 | const mat = new THREE.MeshBasicMaterial({ 20 | transparent: true, 21 | opacity: 0.2, 22 | color: new THREE.Color(0x00ffff), 23 | depthTest: false 24 | }) 25 | const mesh = new THREE.InstancedMesh(cube, mat, matrices.length) 26 | matrices.forEach((m, i) => mesh.setMatrixAt(i, m)) 27 | 28 | return mesh 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/deferredPromise.ts: -------------------------------------------------------------------------------- 1 | export class DeferredPromise extends Promise { 2 | resolve: (value: T | PromiseLike) => void 3 | reject: (reason: T | Error) => void 4 | 5 | initialCallStack: Error['stack'] 6 | 7 | constructor (executor: ConstructorParameters>[0] = () => {}) { 8 | let resolver: (value: T | PromiseLike) => void 9 | let rejector: (reason: T | Error) => void 10 | 11 | super((resolve, reject) => { 12 | resolver = resolve 13 | rejector = reject 14 | return executor(resolve, reject) // Promise magic: this line is unexplicably essential 15 | }) 16 | 17 | this.resolve = resolver! 18 | this.reject = rejector! 19 | 20 | // store call stack for location where instance is created 21 | this.initialCallStack = Error().stack?.split('\n').slice(2).join('\n') 22 | } 23 | 24 | /** @throws error with amended call stack */ 25 | rejectWithError (error: Error) { 26 | error.stack = [error.stack?.split('\n')[0], this.initialCallStack].join('\n') 27 | this.reject(error) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/requestResult.ts: -------------------------------------------------------------------------------- 1 | 2 | export class SuccessResult { 3 | result: T 4 | 5 | constructor (result: T) { 6 | this.result = result 7 | } 8 | 9 | isSuccess (): this is SuccessResult { 10 | return true 11 | } 12 | 13 | isError (): false { 14 | return false 15 | } 16 | } 17 | 18 | export class ErrorResult { 19 | error: string 20 | 21 | constructor (error: string) { 22 | this.error = error 23 | } 24 | 25 | isSuccess (): false { 26 | return false 27 | } 28 | 29 | isError (): this is ErrorResult { 30 | return true 31 | } 32 | } 33 | 34 | export type RequestResult = SuccessResult | ErrorResult 35 | -------------------------------------------------------------------------------- /src/vim-loader/averageBoundingBox.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | /** 4 | * Returns the bounding box of the average center of all meshes. 5 | * Less precise but is more stable against outliers. 6 | */ 7 | export function getAverageBoundingBox (positions: THREE.Vector3[], thresholdSpan = 1000, framingDistanceMultiplier = 2): THREE.Box3 { 8 | if (positions.length === 0) { 9 | return new THREE.Box3() 10 | } 11 | 12 | const { centroid, aabb } = calculateCentroidAndBoundingBox(positions) 13 | const span = aabb.getSize(new THREE.Vector3()).length() 14 | const center = span > thresholdSpan ? centroid : aabb.getCenter(new THREE.Vector3()) 15 | 16 | const avgDist = new THREE.Vector3() 17 | for (const pos of positions) { 18 | avgDist.set( 19 | avgDist.x + Math.abs(pos.x - center.x), 20 | avgDist.y + Math.abs(pos.y - center.y), 21 | avgDist.z + Math.abs(pos.z - center.z)) 22 | } 23 | 24 | const scaledDist = avgDist.multiplyScalar(framingDistanceMultiplier / positions.length) 25 | return new THREE.Box3( 26 | center.clone().sub(scaledDist), 27 | center.clone().add(scaledDist) 28 | ) 29 | } 30 | 31 | function calculateCentroidAndBoundingBox (positions: THREE.Vector3[]): { centroid: THREE.Vector3, aabb: THREE.Box3 } { 32 | const sum = new THREE.Vector3() 33 | const aabb = new THREE.Box3() 34 | 35 | for (const pos of positions) { 36 | sum.add(pos) 37 | aabb.expandByPoint(pos) 38 | } 39 | 40 | const centroid = sum.divideScalar(positions.length) 41 | return { centroid, aabb } 42 | } 43 | -------------------------------------------------------------------------------- /src/vim-loader/colorAttributes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { MergedSubmesh } from './mesh' 7 | import { Vim } from './vim' 8 | import { InsertableSubmesh } from './progressive/insertableSubmesh' 9 | import { AttributeTarget } from './objectAttributes' 10 | 11 | export class ColorAttribute { 12 | readonly vim: Vim 13 | private _meshes: AttributeTarget[] | undefined 14 | private _value: THREE.Color | undefined 15 | 16 | constructor ( 17 | meshes: AttributeTarget[] | undefined, 18 | value: THREE.Color | undefined, 19 | vim: Vim | undefined 20 | ) { 21 | this._meshes = meshes 22 | this._value = value 23 | this.vim = vim 24 | } 25 | 26 | updateMeshes (meshes: AttributeTarget[] | undefined) { 27 | this._meshes = meshes 28 | if (this._value !== undefined) { 29 | this.apply(this._value) 30 | } 31 | } 32 | 33 | get value () { 34 | return this._value 35 | } 36 | 37 | apply (color: THREE.Color | undefined) { 38 | this._value = color 39 | if (!this._meshes) return 40 | 41 | for (let m = 0; m < this._meshes.length; m++) { 42 | const sub = this._meshes[m] 43 | if (sub.merged) { 44 | this.applyMergedColor(sub as MergedSubmesh, color) 45 | } else { 46 | this.applyInstancedColor(sub, color) 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Writes new color to the appropriate section of merged mesh color buffer. 53 | * @param index index of the merged mesh instance 54 | * @param color rgb representation of the color to apply 55 | */ 56 | private applyMergedColor (sub: MergedSubmesh, color: THREE.Color | undefined) { 57 | if (!color) { 58 | this.resetMergedColor(sub) 59 | return 60 | } 61 | 62 | const start = sub.meshStart 63 | const end = sub.meshEnd 64 | 65 | const colors = sub.three.geometry.getAttribute( 66 | 'color' 67 | ) as THREE.BufferAttribute 68 | 69 | const indices = sub.three.geometry.index! 70 | 71 | // Save colors to be able to reset. 72 | if (sub instanceof InsertableSubmesh) { 73 | let c = 0 74 | const previous = new Float32Array((end - start) * 3) 75 | for (let i = start; i < end; i++) { 76 | const v = indices.getX(i) 77 | previous[c++] = colors.getX(v) 78 | previous[c++] = colors.getY(v) 79 | previous[c++] = colors.getZ(v) 80 | } 81 | sub.saveColors(previous) 82 | } 83 | 84 | for (let i = start; i < end; i++) { 85 | const v = indices.getX(i) 86 | // alpha is left to its current value 87 | colors.setXYZ(v, color.r, color.g, color.b) 88 | } 89 | colors.needsUpdate = true 90 | colors.updateRange.offset = 0 91 | colors.updateRange.count = -1 92 | } 93 | 94 | /** 95 | * Repopulates the color buffer of the merged mesh from original g3d data. 96 | * @param index index of the merged mesh instance 97 | */ 98 | private resetMergedColor (sub: MergedSubmesh) { 99 | if (!this.vim) return 100 | if (sub instanceof InsertableSubmesh) { 101 | this.resetMergedInsertableColor(sub) 102 | return 103 | } 104 | 105 | const colors = sub.three.geometry.getAttribute( 106 | 'color' 107 | ) as THREE.BufferAttribute 108 | 109 | const indices = sub.three.geometry.index! 110 | let mergedIndex = sub.meshStart 111 | 112 | const g3d = this.vim.g3d 113 | const g3dMesh = g3d.instanceMeshes[sub.instance] 114 | const subStart = g3d.getMeshSubmeshStart(g3dMesh) 115 | const subEnd = g3d.getMeshSubmeshEnd(g3dMesh) 116 | 117 | for (let sub = subStart; sub < subEnd; sub++) { 118 | const start = g3d.getSubmeshIndexStart(sub) 119 | const end = g3d.getSubmeshIndexEnd(sub) 120 | const color = g3d.getSubmeshColor(sub) 121 | for (let i = start; i < end; i++) { 122 | const v = indices.getX(mergedIndex) 123 | colors.setXYZ(v, color[0], color[1], color[2]) 124 | mergedIndex++ 125 | } 126 | } 127 | colors.needsUpdate = true 128 | colors.updateRange.offset = 0 129 | colors.updateRange.count = -1 130 | } 131 | 132 | private resetMergedInsertableColor (sub: InsertableSubmesh) { 133 | const previous = sub.popColors() 134 | if (previous === undefined) return 135 | 136 | const indices = sub.three.geometry.index! 137 | const colors = sub.three.geometry.getAttribute( 138 | 'color' 139 | ) as THREE.BufferAttribute 140 | 141 | let c = 0 142 | for (let i = sub.meshStart; i < sub.meshEnd; i++) { 143 | const v = indices.getX(i) 144 | colors.setXYZ(v, previous[c], previous[c + 1], previous[c + 2]) 145 | c += 3 146 | } 147 | 148 | colors.needsUpdate = true 149 | colors.updateRange.offset = 0 150 | colors.updateRange.count = -1 151 | } 152 | 153 | /** 154 | * Adds an instanceColor buffer to the instanced mesh and sets new color for given instance 155 | * @param index index of the instanced instance 156 | * @param color rgb representation of the color to apply 157 | */ 158 | private applyInstancedColor (sub: AttributeTarget, color: THREE.Color | undefined) { 159 | const colors = this.getOrAddInstanceColorAttribute( 160 | sub.three as THREE.InstancedMesh 161 | ) 162 | if (color) { 163 | // Set instance to use instance color provided 164 | colors.setXYZ(sub.index, color.r, color.g, color.b) 165 | // Set attributes dirty 166 | colors.needsUpdate = true 167 | colors.updateRange.offset = 0 168 | colors.updateRange.count = -1 169 | } 170 | } 171 | 172 | private getOrAddInstanceColorAttribute (mesh: THREE.InstancedMesh) { 173 | if (mesh.instanceColor && 174 | mesh.instanceColor.count <= mesh.instanceMatrix.count 175 | ) { 176 | return mesh.instanceColor 177 | } 178 | 179 | // mesh.count is not always === to capacity so we use instanceMatrix.count 180 | const count = mesh.instanceMatrix.count 181 | // Add color instance attribute 182 | const colors = new Float32Array(count * 3) 183 | const attribute = new THREE.InstancedBufferAttribute(colors, 3) 184 | mesh.instanceColor = attribute 185 | return attribute 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/vim-loader/materials/isolationMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** 8 | * Material for isolation mode 9 | * Non visible item appear as transparent. 10 | * Visible items are flat shaded with a basic pseudo lighting. 11 | * Supports object coloring for visible objects. 12 | * Non-visible objects use fillColor. 13 | */ 14 | export function createIsolationMaterial () { 15 | return new THREE.ShaderMaterial({ 16 | uniforms: { 17 | opacity: { value: 0.1 }, 18 | fillColor: { value: new THREE.Vector3(0, 0, 0) } 19 | }, 20 | vertexColors: true, 21 | transparent: true, 22 | clipping: true, 23 | vertexShader: /* glsl */ ` 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | // VISIBILITY 30 | // Instance or vertex attribute to hide objects 31 | // Used as instance attribute for instanced mesh and as vertex attribute for merged meshes. 32 | attribute float ignore; 33 | 34 | // Passed to fragment to discard them 35 | varying float vIgnore; 36 | varying vec3 vPosition; 37 | 38 | 39 | // COLORING 40 | varying vec3 vColor; 41 | 42 | // attribute for color override 43 | // merged meshes use it as vertex attribute 44 | // instanced meshes use it as an instance attribute 45 | attribute float colored; 46 | 47 | // There seems to be an issue where setting mehs.instanceColor 48 | // doesn't properly set USE_INSTANCING_COLOR 49 | // so we always use it as a fix 50 | #ifndef USE_INSTANCING_COLOR 51 | attribute vec3 instanceColor; 52 | #endif 53 | 54 | void main() { 55 | #include 56 | #include 57 | #include 58 | #include 59 | 60 | // VISIBILITY 61 | // Set frag ignore from instance or vertex attribute 62 | vIgnore = ignore; 63 | 64 | // COLORING 65 | vColor = color.xyz; 66 | 67 | // colored == 1 -> instance color 68 | // colored == 0 -> vertex color 69 | #ifdef USE_INSTANCING 70 | vColor.xyz = colored * instanceColor.xyz + (1.0f - colored) * color.xyz; 71 | #endif 72 | 73 | 74 | // ORDERING 75 | if(vIgnore > 0.0f){ 76 | gl_Position.z = 1.0f; 77 | }else{ 78 | gl_Position.z = -1.0f; 79 | } 80 | 81 | // LIGHTING 82 | vPosition = vec3(mvPosition ) / mvPosition .w; 83 | } 84 | `, 85 | fragmentShader: /* glsl */ ` 86 | #include 87 | varying float vIgnore; 88 | uniform float opacity; 89 | uniform vec3 fillColor; 90 | varying vec3 vPosition; 91 | varying vec3 vColor; 92 | 93 | void main() { 94 | #include 95 | 96 | if (vIgnore > 0.0f){ 97 | gl_FragColor = vec4(fillColor, opacity); 98 | } 99 | else{ 100 | gl_FragColor = vec4(vColor.x, vColor.y, vColor.z, 1.0f); 101 | 102 | // LIGHTING 103 | vec3 normal = normalize( cross(dFdx(vPosition), dFdy(vPosition)) ); 104 | float light = dot(normal, normalize(vec3(1.4142f, 1.732f, 2.2360f))); 105 | light = 0.5 + (light *0.5); 106 | gl_FragColor.xyz *= light; 107 | } 108 | } 109 | ` 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /src/vim-loader/materials/maskMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** 8 | * Material used for selection outline it only renders selection in white and discards the rests. 9 | */ 10 | export function createMaskMaterial () { 11 | return new THREE.ShaderMaterial({ 12 | uniforms: {}, 13 | clipping: true, 14 | vertexShader: ` 15 | #include 16 | #include 17 | #include 18 | 19 | // Used as instance attribute for instanced mesh and as vertex attribute for merged meshes. 20 | attribute float selected; 21 | 22 | varying float vKeep; 23 | 24 | void main() { 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | // SELECTION 31 | // selected 32 | vKeep = selected; 33 | } 34 | `, 35 | fragmentShader: ` 36 | #include 37 | varying float vKeep; 38 | 39 | void main() { 40 | #include 41 | if(vKeep == 0.0f) discard; 42 | 43 | gl_FragColor = vec4(1.0f,1.0f,1.0f,1.0f); 44 | } 45 | ` 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/vim-loader/materials/mergeMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | export class MergeMaterial { 8 | material: THREE.ShaderMaterial 9 | 10 | constructor () { 11 | this.material = createMergeMaterial() 12 | } 13 | 14 | get color () { 15 | return this.material.uniforms.color.value 16 | } 17 | 18 | set color (value: THREE.Color) { 19 | this.material.uniforms.color.value.copy(value) 20 | this.material.uniformsNeedUpdate = true 21 | } 22 | 23 | get sourceA () { 24 | return this.material.uniforms.sourceA.value 25 | } 26 | 27 | set sourceA (value: THREE.Texture) { 28 | this.material.uniforms.sourceA.value = value 29 | this.material.uniformsNeedUpdate = true 30 | } 31 | 32 | get sourceB () { 33 | return this.material.uniforms.sourceB.value 34 | } 35 | 36 | set sourceB (value: THREE.Texture) { 37 | this.material.uniforms.sourceB.value = value 38 | this.material.uniformsNeedUpdate = true 39 | } 40 | } 41 | 42 | /** 43 | * Material that Merges current fragment with a source texture. 44 | */ 45 | export function createMergeMaterial () { 46 | return new THREE.ShaderMaterial({ 47 | uniforms: { 48 | sourceA: { value: null }, 49 | sourceB: { value: null }, 50 | color: { value: new THREE.Color(0xffffff) } 51 | }, 52 | vertexShader: ` 53 | varying vec2 vUv; 54 | void main() { 55 | vUv = uv; 56 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 57 | } 58 | `, 59 | fragmentShader: ` 60 | uniform vec3 color; 61 | uniform sampler2D sourceA; 62 | uniform sampler2D sourceB; 63 | varying vec2 vUv; 64 | 65 | void main() { 66 | vec4 A = texture2D(sourceA, vUv); 67 | vec4 B = texture2D(sourceB, vUv); 68 | 69 | gl_FragColor = vec4(mix(A.xyz, color, B.x),1.0f); 70 | } 71 | ` 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/vim-loader/materials/outlineMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** Outline Material based on edge detection. */ 8 | export class OutlineMaterial { 9 | material: THREE.ShaderMaterial 10 | private _camera: 11 | | THREE.PerspectiveCamera 12 | | THREE.OrthographicCamera 13 | | undefined 14 | 15 | private _resolution: THREE.Vector2 16 | 17 | constructor ( 18 | options?: Partial<{ 19 | sceneBuffer: THREE.Texture 20 | resolution: THREE.Vector2 21 | camera: THREE.PerspectiveCamera | THREE.OrthographicCamera 22 | }> 23 | ) { 24 | this.material = createOutlineMaterial() 25 | this._resolution = options?.resolution ?? new THREE.Vector2(1, 1) 26 | this.resolution = this._resolution 27 | if (options?.sceneBuffer) { 28 | this.sceneBuffer = options.sceneBuffer 29 | } 30 | this.camera = options?.camera 31 | } 32 | 33 | get resolution () { 34 | return this._resolution 35 | } 36 | 37 | set resolution (value: THREE.Vector2) { 38 | this.material.uniforms.screenSize.value.set( 39 | value?.x ?? 1, 40 | value?.y ?? 1, 41 | 1 / value?.x ?? 1, 42 | 1 / value?.y ?? 1 43 | ) 44 | 45 | this._resolution = value 46 | } 47 | 48 | get camera () { 49 | return this._camera 50 | } 51 | 52 | set camera ( 53 | value: THREE.PerspectiveCamera | THREE.OrthographicCamera | undefined 54 | ) { 55 | this.material.uniforms.cameraNear.value = value?.near ?? 1 56 | this.material.uniforms.cameraFar.value = value?.far ?? 1000 57 | this._camera = value 58 | } 59 | 60 | get strokeBlur () { 61 | return this.material.uniforms.strokeBlur.value 62 | } 63 | 64 | set strokeBlur (value: number) { 65 | this.material.uniforms.strokeBlur.value = value 66 | } 67 | 68 | get strokeBias () { 69 | return this.material.uniforms.strokeBias.value 70 | } 71 | 72 | set strokeBias (value: number) { 73 | this.material.uniforms.strokeBias.value = value 74 | } 75 | 76 | get strokeMultiplier () { 77 | return this.material.uniforms.strokeMultiplier.value 78 | } 79 | 80 | set strokeMultiplier (value: number) { 81 | this.material.uniforms.strokeMultiplier.value = value 82 | } 83 | 84 | get color () { 85 | return this.material.uniforms.outlineColor.value 86 | } 87 | 88 | set color (value: THREE.Color) { 89 | this.material.uniforms.outlineColor.value.set(value) 90 | } 91 | 92 | get sceneBuffer () { 93 | return this.material.uniforms.sceneBuffer.value 94 | } 95 | 96 | set sceneBuffer (value: THREE.Texture) { 97 | this.material.uniforms.sceneBuffer.value = value 98 | } 99 | 100 | get depthBuffer () { 101 | return this.material.uniforms.depthBuffer.value 102 | } 103 | 104 | set depthBuffer (value: THREE.Texture) { 105 | this.material.uniforms.depthBuffer.value = value 106 | } 107 | 108 | dispose () { 109 | this.material.dispose() 110 | } 111 | } 112 | 113 | /** 114 | * This material =computes outline using the depth buffer and combines it with the scene buffer to create a final scene. 115 | */ 116 | export function createOutlineMaterial () { 117 | return new THREE.ShaderMaterial({ 118 | uniforms: { 119 | // Input buffers 120 | sceneBuffer: { value: null }, 121 | depthBuffer: { value: null }, 122 | 123 | // Input parameters 124 | cameraNear: { value: 1 }, 125 | cameraFar: { value: 1000 }, 126 | screenSize: { 127 | value: new THREE.Vector4(1, 1, 1, 1) 128 | }, 129 | 130 | // Options 131 | outlineColor: { value: new THREE.Color(0xffffff) }, 132 | strokeMultiplier: { value: 2 }, 133 | strokeBias: { value: 2 }, 134 | strokeBlur: { value: 3 } 135 | }, 136 | vertexShader: ` 137 | varying vec2 vUv; 138 | void main() { 139 | vUv = uv; 140 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 141 | } 142 | `, 143 | fragmentShader: ` 144 | #include 145 | // The above include imports "perspectiveDepthToViewZ" 146 | // and other GLSL functions from ThreeJS we need for reading depth. 147 | uniform sampler2D depthBuffer; 148 | uniform float cameraNear; 149 | uniform float cameraFar; 150 | uniform vec4 screenSize; 151 | uniform vec3 outlineColor; 152 | uniform float strokeMultiplier; 153 | uniform float strokeBias; 154 | uniform int strokeBlur; 155 | 156 | varying vec2 vUv; 157 | 158 | // Helper functions for reading from depth buffer. 159 | float readDepth (sampler2D depthSampler, vec2 coord) { 160 | float fragCoordZ = texture2D(depthSampler, coord).x; 161 | float viewZ = perspectiveDepthToViewZ( fragCoordZ, cameraNear, cameraFar ); 162 | return viewZToOrthographicDepth( viewZ, cameraNear, cameraFar ); 163 | } 164 | float getLinearDepth(vec3 pos) { 165 | return -(viewMatrix * vec4(pos, 1.0)).z; 166 | } 167 | 168 | float getLinearScreenDepth(sampler2D map) { 169 | vec2 uv = gl_FragCoord.xy * screenSize.zw; 170 | return readDepth(map,uv); 171 | } 172 | // Helper functions for reading normals and depth of neighboring pixels. 173 | float getPixelDepth(int x, int y) { 174 | // screenSize.zw is pixel size 175 | // vUv is current position 176 | return readDepth(depthBuffer, vUv + screenSize.zw * vec2(x, y)); 177 | } 178 | 179 | float saturate(float num) { 180 | return clamp(num, 0.0, 1.0); 181 | } 182 | 183 | void main() { 184 | float depth = getPixelDepth(0, 0); 185 | 186 | // Get the difference between depth of neighboring pixels and current. 187 | float depthDiff = 0.0; 188 | int start = -strokeBlur / 2; 189 | for(int i=0; i < strokeBlur; i ++){ 190 | for(int j=0; j < strokeBlur; j ++){ 191 | depthDiff += abs(depth - getPixelDepth(start +i, start + j)); 192 | } 193 | } 194 | 195 | depthDiff = depthDiff / (float(strokeBlur*strokeBlur) -1.0); 196 | 197 | depthDiff = depthDiff * strokeMultiplier; 198 | depthDiff = saturate(depthDiff); 199 | depthDiff = pow(depthDiff, strokeBias); 200 | 201 | float outline = depthDiff; 202 | 203 | // Combine outline with scene color. 204 | vec4 outlineColor = vec4(outlineColor, 1.0f); 205 | gl_FragColor = vec4(mix(vec4(0.0,0.0,0.0,0.0), outlineColor, outline)); 206 | } 207 | ` 208 | }) 209 | } 210 | -------------------------------------------------------------------------------- /src/vim-loader/materials/simpleMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** 8 | * Material for isolation mode 9 | * Non visible item appear as transparent. 10 | * Visible items are flat shaded with a basic pseudo lighting. 11 | * Supports object coloring for visible objects. 12 | * Non-visible objects use fillColor. 13 | */ 14 | export function createSimpleMaterial () { 15 | return new THREE.ShaderMaterial({ 16 | uniforms: { 17 | opacity: { value: 0.1 }, 18 | fillColor: { value: new THREE.Vector3(0, 0, 0) } 19 | }, 20 | vertexColors: true, 21 | // transparent: true, 22 | clipping: true, 23 | vertexShader: /* glsl */ ` 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | // VISIBILITY 30 | // Instance or vertex attribute to hide objects 31 | // Used as instance attribute for instanced mesh and as vertex attribute for merged meshes. 32 | attribute float ignore; 33 | 34 | // Passed to fragment to discard them 35 | varying float vIgnore; 36 | varying vec3 vPosition; 37 | 38 | 39 | // COLORING 40 | varying vec3 vColor; 41 | 42 | // attribute for color override 43 | // merged meshes use it as vertex attribute 44 | // instanced meshes use it as an instance attribute 45 | attribute float colored; 46 | 47 | // There seems to be an issue where setting mehs.instanceColor 48 | // doesn't properly set USE_INSTANCING_COLOR 49 | // so we always use it as a fix 50 | #ifndef USE_INSTANCING_COLOR 51 | attribute vec3 instanceColor; 52 | #endif 53 | 54 | void main() { 55 | #include 56 | #include 57 | #include 58 | #include 59 | 60 | // VISIBILITY 61 | // Set frag ignore from instance or vertex attribute 62 | vIgnore = ignore; 63 | 64 | // COLORING 65 | vColor = color.xyz; 66 | 67 | // colored == 1 -> instance color 68 | // colored == 0 -> vertex color 69 | #ifdef USE_INSTANCING 70 | vColor.xyz = colored * instanceColor.xyz + (1.0f - colored) * color.xyz; 71 | #endif 72 | 73 | gl_Position.z = -10.0f; 74 | 75 | // LIGHTING 76 | vPosition = vec3(mvPosition ) / mvPosition .w; 77 | } 78 | `, 79 | fragmentShader: /* glsl */ ` 80 | #include 81 | varying float vIgnore; 82 | varying vec3 vPosition; 83 | varying vec3 vColor; 84 | 85 | void main() { 86 | #include 87 | 88 | if (vIgnore > 0.0f){ 89 | discard; 90 | } 91 | else{ 92 | gl_FragColor = vec4(vColor.x, vColor.y, vColor.z, 1.0f); 93 | 94 | // LIGHTING 95 | vec3 normal = normalize( cross(dFdx(vPosition), dFdy(vPosition)) ); 96 | float light = dot(normal, normalize(vec3(1.4142f, 1.732f, 2.2360f))); 97 | light = 0.5 + (light *0.5); 98 | gl_FragColor.xyz *= light; 99 | } 100 | } 101 | ` 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /src/vim-loader/materials/skyboxMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** 8 | * Material for the skybox 9 | */ 10 | export class SkyboxMaterial extends THREE.ShaderMaterial { 11 | get skyColor (): THREE.Color { 12 | return this.uniforms.skyColor.value 13 | } 14 | 15 | set skyColor (value: THREE.Color) { 16 | this.uniforms.skyColor.value = value 17 | this.uniformsNeedUpdate = true 18 | } 19 | 20 | get groundColor () { 21 | return this.uniforms.groundColor.value 22 | } 23 | 24 | set groundColor (value: THREE.Color) { 25 | this.uniforms.groundColor.value = value 26 | this.uniformsNeedUpdate = true 27 | } 28 | 29 | get sharpness () { 30 | return this.uniforms.sharpness.value 31 | } 32 | 33 | set sharpness (value: number) { 34 | this.uniforms.sharpness.value = value 35 | this.uniformsNeedUpdate = true 36 | } 37 | 38 | constructor ( 39 | skyColor: THREE.Color = new THREE.Color(0.68, 0.85, 0.9), 40 | groundColor: THREE.Color = new THREE.Color(0.8, 0.7, 0.5), 41 | sharpness: number = 2) { 42 | super({ 43 | uniforms: { 44 | skyColor: { value: skyColor }, 45 | groundColor: { value: groundColor }, 46 | sharpness: { value: sharpness } 47 | }, 48 | vertexShader: /* glsl */ ` 49 | varying vec3 vPosition; 50 | varying vec3 vCameraPosition; 51 | 52 | void main() { 53 | // Compute vertex position 54 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0 ); 55 | gl_Position = projectionMatrix * mvPosition; 56 | 57 | // Set z to camera.far so that the skybox is always rendered behind everything else 58 | gl_Position.z = gl_Position.w; 59 | 60 | // Pass the vertex world position to the fragment shader 61 | vPosition = (modelMatrix * vec4(position, 1.0)).xyz; 62 | 63 | // Pass the camera position to the fragment shader 64 | mat4 inverseViewMatrix = inverse(viewMatrix); 65 | vCameraPosition = (inverseViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; 66 | } 67 | `, 68 | fragmentShader: /* glsl */ ` 69 | uniform vec3 skyColor; 70 | uniform vec3 groundColor; 71 | uniform float sharpness; 72 | 73 | varying vec3 vPosition; 74 | varying vec3 vCameraPosition; 75 | 76 | void main() { 77 | // Define the up vector 78 | vec3 up = vec3(0.0, 1.0, 0.0); 79 | 80 | // Calculate the direction from the pixel to the camera 81 | vec3 directionToCamera = normalize(vCameraPosition - vPosition); 82 | 83 | // Calculate the dot product between the normal and the up vector 84 | float dotProduct = dot(directionToCamera, up); 85 | 86 | // Normalize the dot product to be between 0 and 1 87 | float t = (dotProduct + 1.0) / 2.0; 88 | 89 | // Apply a power function to create a sharper transition 90 | t = pow(t, sharpness); 91 | 92 | // Interpolate between colors 93 | vec3 pastelSkyBlue = vec3(0.68, 0.85, 0.9); // Light sky blue pastel 94 | vec3 pastelEarthyBrown = vec3(0.8, 0.7, 0.5); // Light earthy brown pastel 95 | vec3 color = mix(skyColor, groundColor, t); 96 | 97 | // Output the final color 98 | gl_FragColor = vec4(color, 1.0); 99 | } 100 | ` 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/vim-loader/materials/transferMaterial.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader/materials 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** 8 | * This material simply sample and returns the value at each texel position of the texture. 9 | */ 10 | export function createTransferMaterial () { 11 | return new THREE.ShaderMaterial({ 12 | uniforms: { 13 | source: { value: null } 14 | }, 15 | vertexShader: ` 16 | varying vec2 vUv; 17 | void main() { 18 | vUv = uv; 19 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 20 | } 21 | `, 22 | fragmentShader: ` 23 | uniform sampler2D source; 24 | varying vec2 vUv; 25 | 26 | void main() { 27 | gl_FragColor = texture2D(source, vUv); 28 | } 29 | ` 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/vim-loader/mesh.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { InsertableSubmesh } from './progressive/insertableSubmesh' 7 | import { Vim } from './vim' 8 | import { InstancedSubmesh } from './progressive/instancedSubmesh' 9 | 10 | /** 11 | * Wrapper around THREE.Mesh 12 | * Keeps track of what VIM instances are part of this mesh. 13 | * Is either merged on instanced. 14 | */ 15 | export class Mesh { 16 | /** 17 | * the wrapped THREE mesh 18 | */ 19 | mesh: THREE.Mesh 20 | 21 | /** 22 | * Vim file from which this mesh was created. 23 | */ 24 | vim: Vim | undefined 25 | 26 | /** 27 | * Whether the mesh is merged or not. 28 | */ 29 | merged: boolean 30 | 31 | /** 32 | * Indices of the g3d instances that went into creating the mesh 33 | */ 34 | instances: number[] 35 | 36 | /** 37 | * startPosition of each submesh on a merged mesh. 38 | */ 39 | submeshes: number[] 40 | /** 41 | * bounding box of each instance 42 | */ 43 | boxes: THREE.Box3[] 44 | 45 | /** 46 | * Set to true to ignore SetMaterial calls. 47 | */ 48 | ignoreSceneMaterial: boolean 49 | 50 | /** 51 | * Total bounding box for this mesh. 52 | */ 53 | boundingBox: THREE.Box3 54 | 55 | /** 56 | * initial material. 57 | */ 58 | private _material: THREE.Material | THREE.Material[] 59 | 60 | private constructor ( 61 | mesh: THREE.Mesh, 62 | instance: number[], 63 | boxes: THREE.Box3[] 64 | ) { 65 | this.mesh = mesh 66 | this.mesh.userData.vim = this 67 | this.instances = instance 68 | this.boxes = boxes 69 | this.boundingBox = this.unionAllBox(boxes) 70 | } 71 | 72 | static createMerged ( 73 | mesh: THREE.Mesh, 74 | instances: number[], 75 | boxes: THREE.Box3[], 76 | submeshes: number[] 77 | ) { 78 | const result = new Mesh(mesh, instances, boxes) 79 | result.merged = true 80 | result.submeshes = submeshes 81 | return result 82 | } 83 | 84 | static createInstanced ( 85 | mesh: THREE.Mesh, 86 | instances: number[], 87 | boxes: THREE.Box3[] 88 | ) { 89 | const result = new Mesh(mesh, instances, boxes) 90 | result.merged = false 91 | return result 92 | } 93 | 94 | /** 95 | * Overrides mesh material, set to undefine to restore initial material. 96 | */ 97 | setMaterial (value: THREE.Material) { 98 | if (this._material === value) return 99 | if (this.ignoreSceneMaterial) return 100 | 101 | if (value) { 102 | if (!this._material) { 103 | this._material = this.mesh.material 104 | } 105 | this.mesh.material = value 106 | } else { 107 | if (this._material) { 108 | this.mesh.material = this._material 109 | this._material = undefined 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Returns submesh for given index. 116 | */ 117 | getSubMesh (index: number) { 118 | return new StandardSubmesh(this, index) 119 | } 120 | 121 | /** 122 | * Returns submesh corresponding to given face on a merged mesh. 123 | */ 124 | getSubmeshFromFace (faceIndex: number) { 125 | if (!this.merged) { 126 | throw new Error('Can only be called when mesh.merged = true') 127 | } 128 | const index = this.binarySearch(this.submeshes, faceIndex * 3) 129 | return new StandardSubmesh(this, index) 130 | } 131 | 132 | /** 133 | * 134 | * @returns Returns all submeshes 135 | */ 136 | getSubmeshes () { 137 | return this.instances.map((s, i) => new StandardSubmesh(this, i)) 138 | } 139 | 140 | private binarySearch (array: number[], element: number) { 141 | let m = 0 142 | let n = array.length - 1 143 | while (m <= n) { 144 | const k = (n + m) >> 1 145 | const cmp = element - array[k] 146 | if (cmp > 0) { 147 | m = k + 1 148 | } else if (cmp < 0) { 149 | n = k - 1 150 | } else { 151 | return k 152 | } 153 | } 154 | return m - 1 155 | } 156 | 157 | private unionAllBox (boxes: THREE.Box3[]) { 158 | const box = boxes[0].clone() 159 | for (let i = 1; i < boxes.length; i++) { 160 | box.union(boxes[i]) 161 | } 162 | return box 163 | } 164 | } 165 | 166 | // eslint-disable-next-line no-use-before-define 167 | export type MergedSubmesh = StandardSubmesh | InsertableSubmesh 168 | export type Submesh = MergedSubmesh | InstancedSubmesh 169 | 170 | export class SimpleInstanceSubmesh { 171 | mesh: THREE.InstancedMesh 172 | get three () { return this.mesh } 173 | index : number 174 | readonly merged = false 175 | 176 | constructor (mesh: THREE.InstancedMesh, index : number) { 177 | this.mesh = mesh 178 | this.index = index 179 | } 180 | } 181 | 182 | export class StandardSubmesh { 183 | mesh: Mesh 184 | index: number 185 | 186 | constructor (mesh: Mesh, index: number) { 187 | this.mesh = mesh 188 | this.index = index 189 | } 190 | 191 | equals (other: Submesh) { 192 | return this.mesh === other.mesh && this.index === other.index 193 | } 194 | 195 | /** 196 | * Returns parent three mesh. 197 | */ 198 | get three () { 199 | return this.mesh.mesh 200 | } 201 | 202 | /** 203 | * True if parent mesh is merged. 204 | */ 205 | get merged () { 206 | return this.mesh.merged 207 | } 208 | 209 | /** 210 | * Returns vim instance associated with this submesh. 211 | */ 212 | get instance () { 213 | return this.mesh.instances[this.index] 214 | } 215 | 216 | /** 217 | * Returns bounding box for this submesh. 218 | */ 219 | get boundingBox () { 220 | return this.mesh.boxes[this.index] 221 | } 222 | 223 | /** 224 | * Returns starting position in parent mesh for merged mesh. 225 | */ 226 | get meshStart () { 227 | return this.mesh.submeshes[this.index] 228 | } 229 | 230 | /** 231 | * Returns ending position in parent mesh for merged mesh. 232 | */ 233 | get meshEnd () { 234 | return this.index + 1 < this.mesh.submeshes.length 235 | ? this.mesh.submeshes[this.index + 1] 236 | : this.three.geometry.index!.count 237 | } 238 | 239 | /** 240 | * Returns vim object for this submesh. 241 | */ 242 | get object () { 243 | return this.mesh.vim.getObjectFromInstance(this.instance) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/vim-loader/objectAttributes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { MergedSubmesh, SimpleInstanceSubmesh, Submesh } from './mesh' 7 | 8 | export type AttributeTarget = Submesh | SimpleInstanceSubmesh 9 | 10 | export class ObjectAttribute { 11 | readonly vertexAttribute: string 12 | readonly instanceAttribute: string 13 | readonly defaultValue: T 14 | readonly toNumber: (value: T) => number 15 | 16 | private _value: T 17 | private _meshes: AttributeTarget[] | undefined 18 | 19 | constructor ( 20 | value: T, 21 | vertexAttribute: string, 22 | instanceAttribute: string, 23 | meshes: AttributeTarget[] | undefined, 24 | toNumber: (value: T) => number 25 | ) { 26 | this._value = value 27 | this.defaultValue = value 28 | this.vertexAttribute = vertexAttribute 29 | this.instanceAttribute = instanceAttribute 30 | this._meshes = meshes 31 | this.toNumber = toNumber 32 | } 33 | 34 | updateMeshes (meshes: AttributeTarget[] | undefined) { 35 | this._meshes = meshes 36 | const v = this._value 37 | this._value = this.defaultValue 38 | this.apply(v) 39 | } 40 | 41 | get value () { 42 | return this._value 43 | } 44 | 45 | apply (value: T) { 46 | if (this._value === value) return false 47 | this._value = value 48 | if (!this._meshes) return false 49 | const number = this.toNumber(value) 50 | 51 | for (let m = 0; m < this._meshes.length; m++) { 52 | const sub = this._meshes[m] 53 | if (sub.merged) { 54 | this.applyMerged(sub as MergedSubmesh, number) 55 | } else { 56 | this.applyInstanced(sub, number) 57 | } 58 | } 59 | return true 60 | } 61 | 62 | private applyInstanced (sub: AttributeTarget, number: number) { 63 | const mesh = sub.three as THREE.InstancedMesh 64 | const geometry = mesh.geometry 65 | let attribute = geometry.getAttribute( 66 | this.instanceAttribute 67 | ) as THREE.BufferAttribute 68 | 69 | if (!attribute || attribute.count < mesh.instanceMatrix.count) { 70 | // mesh.count is not always === to capacity so we use instanceMatrix.count 71 | const array = new Float32Array(mesh.instanceMatrix.count) 72 | attribute = new THREE.InstancedBufferAttribute(array, 1) 73 | geometry.setAttribute(this.instanceAttribute, attribute) 74 | } 75 | attribute.setX(sub.index, number) 76 | attribute.needsUpdate = true 77 | attribute.updateRange.offset = 0 78 | attribute.updateRange.count = -1 79 | } 80 | 81 | private applyMerged (sub: MergedSubmesh, number: number) { 82 | const geometry = sub.three.geometry 83 | const positions = geometry.getAttribute('position') 84 | 85 | let attribute = geometry.getAttribute( 86 | this.vertexAttribute 87 | ) as THREE.BufferAttribute 88 | 89 | if (!attribute) { 90 | // Computed count here is not the same as positions.count 91 | // Positions.count is used to tell the render up to where to render. 92 | const count = positions.array.length / positions.itemSize 93 | const array = new Float32Array(count) 94 | attribute = new THREE.Float32BufferAttribute(array, 1) 95 | geometry.setAttribute(this.vertexAttribute, attribute) 96 | } 97 | 98 | const start = sub.meshStart 99 | const end = sub.meshEnd 100 | const indices = sub.three.geometry.index! 101 | 102 | for (let i = start; i < end; i++) { 103 | const v = indices.getX(i) 104 | attribute.setX(v, number) 105 | } 106 | attribute.needsUpdate = true 107 | attribute.updateRange.offset = 0 108 | attribute.updateRange.count = -1 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/g3dOffsets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { MeshSection } from 'vim-format' 6 | import { G3dSubset } from './g3dSubset' 7 | 8 | export class G3dMeshCounts { 9 | instances: number = 0 10 | meshes: number = 0 11 | indices: number = 0 12 | vertices: number = 0 13 | } 14 | 15 | /** 16 | * Holds the offsets needed to preallocate geometry for a given meshIndexSubset 17 | */ 18 | export class G3dMeshOffsets { 19 | // inputs 20 | readonly subset: G3dSubset 21 | readonly section: MeshSection 22 | 23 | // computed 24 | readonly counts: G3dMeshCounts 25 | private readonly _indexOffsets: Int32Array 26 | private readonly _vertexOffsets: Int32Array 27 | 28 | /** 29 | * Computes geometry offsets for given subset and section 30 | * @param subset subset for which to compute offsets 31 | * @param section 'opaque' | 'transparent' | 'all' 32 | */ 33 | constructor (subset: G3dSubset, section: MeshSection) { 34 | this.subset = subset 35 | this.section = section 36 | 37 | this.counts = subset.getAttributeCounts(section) 38 | this._indexOffsets = this.computeOffsets(subset, (m) => 39 | subset.getMeshIndexCount(m, section) 40 | ) 41 | this._vertexOffsets = this.computeOffsets(subset, (m) => 42 | subset.getMeshVertexCount(m, section) 43 | ) 44 | } 45 | 46 | private computeOffsets (subset: G3dSubset, getter: (mesh: number) => number) { 47 | const meshCount = subset.getMeshCount() 48 | const offsets = new Int32Array(meshCount) 49 | 50 | for (let i = 1; i < meshCount; i++) { 51 | offsets[i] = offsets[i - 1] + getter(i - 1) 52 | } 53 | return offsets 54 | } 55 | 56 | /** 57 | * Returns the index offset for given mesh and its instances. 58 | * @param mesh subset-based mesh index 59 | */ 60 | getIndexOffset (mesh: number) { 61 | return mesh < this.counts.meshes 62 | ? this._indexOffsets[mesh] 63 | : this.counts.indices 64 | } 65 | 66 | /** 67 | * Returns the vertex offset for given mesh and its instances. 68 | * @param mesh subset-based mesh index 69 | */ 70 | getVertexOffset (mesh: number) { 71 | return mesh < this.counts.meshes 72 | ? this._vertexOffsets[mesh] 73 | : this.counts.vertices 74 | } 75 | 76 | /** 77 | * Returns instance counts of given mesh. 78 | * @param mesh subset-based mesh index 79 | */ 80 | getMeshInstanceCount (mesh: number) { 81 | return this.subset.getMeshInstanceCount(mesh) 82 | } 83 | 84 | /** 85 | * Returns source-based instance for given mesh and index. 86 | * @mesh subset-based mesh index 87 | * @index mesh-based instance index 88 | */ 89 | getMeshInstance (mesh: number, index: number) { 90 | return this.subset.getMeshInstance(mesh, index) 91 | } 92 | 93 | /** 94 | * Returns the source-based mesh index at given index 95 | */ 96 | getSourceMesh (index: number) { 97 | return this.subset.getSourceMesh(index) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/insertableMesh.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { G3d, G3dMesh, G3dMaterial } from 'vim-format' 7 | import { InsertableGeometry } from './insertableGeometry' 8 | import { InsertableSubmesh } from './insertableSubmesh' 9 | import { G3dMeshOffsets } from './g3dOffsets' 10 | import { Vim } from '../../vim-loader/vim' 11 | import { ViewerMaterials } from '../materials/viewerMaterials' 12 | 13 | export class InsertableMesh { 14 | offsets: G3dMeshOffsets 15 | mesh: THREE.Mesh 16 | vim: Vim 17 | 18 | /** 19 | * Whether the mesh is merged or not. 20 | */ 21 | get merged () { 22 | return true 23 | } 24 | 25 | /** 26 | * Whether the mesh is transparent or not. 27 | */ 28 | transparent: boolean 29 | 30 | /** 31 | * Total bounding box for this mesh. 32 | */ 33 | get boundingBox () { 34 | return this.geometry.boundingBox 35 | } 36 | 37 | /** 38 | * Set to true to ignore SetMaterial calls. 39 | */ 40 | ignoreSceneMaterial: boolean 41 | 42 | /** 43 | * initial material. 44 | */ 45 | private _material: THREE.Material | THREE.Material[] | undefined 46 | 47 | geometry: InsertableGeometry 48 | 49 | constructor ( 50 | offsets: G3dMeshOffsets, 51 | materials: G3dMaterial, 52 | transparent: boolean 53 | ) { 54 | this.offsets = offsets 55 | this.transparent = transparent 56 | 57 | this.geometry = new InsertableGeometry(offsets, materials, transparent) 58 | 59 | this._material = transparent 60 | ? ViewerMaterials.getInstance().transparent.material 61 | : ViewerMaterials.getInstance().opaque.material 62 | 63 | this.mesh = new THREE.Mesh(this.geometry.geometry, this._material) 64 | this.mesh.userData.vim = this 65 | // this.mesh.frustumCulled = false 66 | } 67 | 68 | get progress () { 69 | return this.geometry.progress 70 | } 71 | 72 | insert (g3d: G3dMesh, mesh: number) { 73 | const added = this.geometry.insert(g3d, mesh) 74 | if (!this.vim) { 75 | return 76 | } 77 | 78 | for (const i of added) { 79 | this.vim.scene.addSubmesh(new InsertableSubmesh(this, i)) 80 | } 81 | } 82 | 83 | insertFromVim (g3d: G3d, mesh: number) { 84 | this.geometry.insertFromG3d(g3d, mesh) 85 | } 86 | 87 | update () { 88 | this.geometry.update() 89 | } 90 | 91 | clearUpdate () { 92 | this.geometry.flushUpdate() 93 | } 94 | 95 | /** 96 | * Returns submesh corresponding to given face on a merged mesh. 97 | */ 98 | getSubmeshFromFace (faceIndex: number) { 99 | // TODO: not iterate through all submeshes 100 | const hitIndex = faceIndex * 3 101 | for (const [instance, submesh] of this.geometry.submeshes.entries()) { 102 | if (hitIndex >= submesh.start && hitIndex < submesh.end) { 103 | return new InsertableSubmesh(this, instance) 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * 110 | * @returns Returns all submeshes 111 | */ 112 | getSubmeshes () { 113 | const submeshes = new Array( 114 | this.geometry.submeshes.length 115 | ) 116 | for (let i = 0; i < submeshes.length; i++) { 117 | submeshes[i] = new InsertableSubmesh(this, i) 118 | } 119 | return submeshes 120 | } 121 | 122 | /** 123 | * 124 | * @returns Returns submesh for given index. 125 | */ 126 | getSubmesh (index: number) { 127 | // if (this.geometry.submeshes.has(index)) { 128 | return new InsertableSubmesh(this, index) 129 | // } 130 | } 131 | 132 | /** 133 | * Overrides mesh material, set to undefine to restore initial material. 134 | */ 135 | setMaterial (value: THREE.Material) { 136 | if (this._material === value) return 137 | if (this.ignoreSceneMaterial) return 138 | 139 | if (value) { 140 | if (!this._material) { 141 | this._material = this.mesh.material 142 | } 143 | this.mesh.material = value 144 | } else { 145 | if (this._material) { 146 | this.mesh.material = this._material 147 | this._material = undefined 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/insertableSubmesh.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { Submesh } from '../mesh' 6 | import { InsertableMesh } from './insertableMesh' 7 | 8 | export class InsertableSubmesh { 9 | mesh: InsertableMesh 10 | index: number 11 | private _colors: Float32Array 12 | 13 | constructor (mesh: InsertableMesh, index: number) { 14 | this.mesh = mesh 15 | this.index = index 16 | } 17 | 18 | equals (other: Submesh) { 19 | return this.mesh === other.mesh && this.index === other.index 20 | } 21 | 22 | /** 23 | * Returns parent three mesh. 24 | */ 25 | get three () { 26 | return this.mesh.mesh 27 | } 28 | 29 | /** 30 | * True if parent mesh is merged. 31 | */ 32 | get merged () { 33 | return true 34 | } 35 | 36 | private get submesh () { 37 | return this.mesh.geometry.submeshes[this.index] 38 | } 39 | 40 | /** 41 | * Returns vim instance associated with this submesh. 42 | */ 43 | get instance () { 44 | return this.submesh.instance 45 | } 46 | 47 | /** 48 | * Returns bounding box for this submesh. 49 | */ 50 | get boundingBox () { 51 | return this.submesh.boundingBox 52 | } 53 | 54 | /** 55 | * Returns starting position in parent mesh for merged mesh. 56 | */ 57 | get meshStart () { 58 | return this.submesh.start 59 | } 60 | 61 | /** 62 | * Returns ending position in parent mesh for merged mesh. 63 | */ 64 | get meshEnd () { 65 | return this.submesh.end 66 | } 67 | 68 | /** 69 | * Returns vim object for this submesh. 70 | */ 71 | get object () { 72 | return this.mesh.vim.getObjectFromInstance(this.instance) 73 | } 74 | 75 | saveColors (colors: Float32Array) { 76 | if (this._colors) return 77 | this._colors = colors 78 | } 79 | 80 | popColors () { 81 | const result = this._colors 82 | this._colors = undefined 83 | return result 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/instancedMesh.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { Vim } from '../../vim-loader/vim' 7 | import { InstancedSubmesh } from './instancedSubmesh' 8 | import { G3d, G3dMesh } from 'vim-format' 9 | 10 | export class InstancedMesh { 11 | g3dMesh: G3dMesh | G3d 12 | vim: Vim 13 | mesh: THREE.InstancedMesh 14 | 15 | // instances 16 | bimInstances: ArrayLike 17 | meshInstances: ArrayLike 18 | boundingBox: THREE.Box3 19 | boxes: THREE.Box3[] 20 | 21 | // State 22 | ignoreSceneMaterial: boolean 23 | private _material: THREE.Material | THREE.Material[] | undefined 24 | 25 | constructor ( 26 | g3d: G3dMesh | G3d, 27 | mesh: THREE.InstancedMesh, 28 | instances: Array 29 | ) { 30 | this.g3dMesh = g3d 31 | this.mesh = mesh 32 | this.mesh.userData.vim = this 33 | this.bimInstances = 34 | g3d instanceof G3dMesh 35 | ? instances.map((i) => g3d.scene.instanceNodes[i]) 36 | : instances 37 | this.meshInstances = instances 38 | 39 | this.boxes = 40 | g3d instanceof G3dMesh 41 | ? this.importBoundingBoxes() 42 | : this.computeBoundingBoxes() 43 | this.boundingBox = this.computeBoundingBox(this.boxes) 44 | } 45 | 46 | get merged () { 47 | return false 48 | } 49 | 50 | /** 51 | * Returns submesh for given index. 52 | */ 53 | getSubMesh (index: number) { 54 | return new InstancedSubmesh(this, index) 55 | } 56 | 57 | /** 58 | * Returns all submeshes for given index. 59 | */ 60 | getSubmeshes () { 61 | const submeshes = new Array(this.bimInstances.length) 62 | for (let i = 0; i < this.bimInstances.length; i++) { 63 | submeshes[i] = new InstancedSubmesh(this, i) 64 | } 65 | return submeshes 66 | } 67 | 68 | setMaterial (value: THREE.Material) { 69 | if (this._material === value) return 70 | if (this.ignoreSceneMaterial) return 71 | 72 | if (value) { 73 | if (!this._material) { 74 | this._material = this.mesh.material 75 | } 76 | this.mesh.material = value 77 | } else { 78 | if (this._material) { 79 | this.mesh.material = this._material 80 | this._material = undefined 81 | } 82 | } 83 | } 84 | 85 | private computeBoundingBoxes () { 86 | this.mesh.geometry.computeBoundingBox() 87 | 88 | const boxes = new Array(this.mesh.count) 89 | const matrix = new THREE.Matrix4() 90 | for (let i = 0; i < this.mesh.count; i++) { 91 | this.mesh.getMatrixAt(i, matrix) 92 | boxes[i] = this.mesh.geometry.boundingBox.clone().applyMatrix4(matrix) 93 | } 94 | 95 | return boxes 96 | } 97 | 98 | private importBoundingBoxes () { 99 | if (this.g3dMesh instanceof G3d) throw new Error('Wrong type') 100 | const boxes = new Array(this.meshInstances.length) 101 | for (let i = 0; i < this.meshInstances.length; i++) { 102 | const box = new THREE.Box3() 103 | const instance = this.meshInstances[i] 104 | box.min.fromArray(this.g3dMesh.scene.getInstanceMin(instance)) 105 | box.max.fromArray(this.g3dMesh.scene.getInstanceMax(instance)) 106 | boxes[i] = box 107 | } 108 | return boxes 109 | } 110 | 111 | computeBoundingBox (boxes: THREE.Box3[]) { 112 | const box = boxes[0].clone() 113 | for (let i = 1; i < boxes.length; i++) { 114 | box.union(boxes[i]) 115 | } 116 | return box 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/instancedMeshFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { G3d, G3dMesh, G3dMaterial, MeshSection, G3dScene } from 'vim-format' 7 | import { InstancedMesh } from './instancedMesh' 8 | import { ViewerMaterials } from '../materials/viewerMaterials' 9 | import { Geometry } from '../geometry' 10 | 11 | export class InstancedMeshFactory { 12 | materials: G3dMaterial 13 | 14 | constructor (materials: G3dMaterial) { 15 | this.materials = materials 16 | } 17 | 18 | createTransparent (mesh: G3dMesh, instances: number[]) { 19 | return this.createFromVimx(mesh, instances, 'transparent', true) 20 | } 21 | 22 | createOpaque (mesh: G3dMesh, instances: number[]) { 23 | return this.createFromVimx(mesh, instances, 'opaque', false) 24 | } 25 | 26 | createOpaqueFromVim (g3d: G3d, mesh: number, instances: number[]) { 27 | return this.createFromVim(g3d, mesh, instances, 'opaque', false) 28 | } 29 | 30 | createTransparentFromVim (g3d: G3d, mesh: number, instances: number[]) { 31 | return this.createFromVim(g3d, mesh, instances, 'transparent', true) 32 | } 33 | 34 | createFromVimx ( 35 | mesh: G3dMesh, 36 | instances: number[], 37 | section: MeshSection, 38 | transparent: boolean 39 | ) { 40 | if (mesh.getIndexCount(section) <= 1) { 41 | return undefined 42 | } 43 | 44 | const geometry = this.createGeometry( 45 | this.computeIndices(mesh, section), 46 | this.computeVertices(mesh, section), 47 | this.computeColors(mesh, section, transparent ? 4 : 3) 48 | ) 49 | 50 | const material = transparent 51 | ? ViewerMaterials.getInstance().transparent 52 | : ViewerMaterials.getInstance().opaque 53 | 54 | const threeMesh = new THREE.InstancedMesh( 55 | geometry, 56 | material.material, 57 | instances.length 58 | ) 59 | 60 | this.setMatricesFromVimx(threeMesh, mesh.scene, instances) 61 | const result = new InstancedMesh(mesh, threeMesh, instances) 62 | return result 63 | } 64 | 65 | createFromVim ( 66 | g3d: G3d, 67 | mesh: number, 68 | instances: number[] | undefined, 69 | section: MeshSection, 70 | transparent: boolean 71 | ) { 72 | const geometry = Geometry.createGeometryFromMesh( 73 | g3d, 74 | mesh, 75 | section, 76 | transparent 77 | ) 78 | const material = transparent 79 | ? ViewerMaterials.getInstance().transparent 80 | : ViewerMaterials.getInstance().opaque 81 | 82 | const threeMesh = new THREE.InstancedMesh( 83 | geometry, 84 | material.material, 85 | instances?.length ?? g3d.getMeshInstanceCount(mesh) 86 | ) 87 | 88 | this.setMatricesFromVimx(threeMesh, g3d, instances) 89 | const result = new InstancedMesh(g3d, threeMesh, instances) 90 | return result 91 | } 92 | 93 | private createGeometry ( 94 | indices: THREE.Uint32BufferAttribute, 95 | positions: THREE.Float32BufferAttribute, 96 | colors: THREE.Float32BufferAttribute 97 | ) { 98 | const geometry = new THREE.BufferGeometry() 99 | geometry.setIndex(indices) 100 | geometry.setAttribute('position', positions) 101 | geometry.setAttribute('color', colors) 102 | return geometry 103 | } 104 | 105 | private computeIndices (mesh: G3dMesh, section: MeshSection) { 106 | const indexStart = mesh.getIndexStart(section) 107 | const indexCount = mesh.getIndexCount(section) 108 | const vertexOffset = mesh.getVertexStart(section) 109 | const indices = new Uint32Array(indexCount) 110 | for (let i = 0; i < indexCount; i++) { 111 | indices[i] = mesh.chunk.indices[indexStart + i] - vertexOffset 112 | } 113 | return new THREE.Uint32BufferAttribute(indices, 1) 114 | } 115 | 116 | private computeVertices (mesh: G3dMesh, section: MeshSection) { 117 | const vertexStart = mesh.getVertexStart(section) 118 | const vertexEnd = mesh.getVertexEnd(section) 119 | const vertices = mesh.chunk.positions.subarray( 120 | vertexStart * G3d.POSITION_SIZE, 121 | vertexEnd * G3d.POSITION_SIZE 122 | ) 123 | return new THREE.Float32BufferAttribute(vertices, G3d.POSITION_SIZE) 124 | } 125 | 126 | private computeColors ( 127 | mesh: G3dMesh, 128 | section: MeshSection, 129 | colorSize: number 130 | ) { 131 | const colors = new Float32Array(mesh.getVertexCount(section) * colorSize) 132 | 133 | let c = 0 134 | const submeshStart = mesh.getSubmeshStart(section) 135 | const submeshEnd = mesh.getSubmeshEnd(section) 136 | for (let sub = submeshStart; sub < submeshEnd; sub++) { 137 | const mat = mesh.chunk.submeshMaterial[sub] 138 | const color = this.materials.getMaterialColor(mat) 139 | const subVertexCount = mesh.getSubmeshVertexCount(sub) 140 | 141 | for (let i = 0; i < subVertexCount; i++) { 142 | colors[c] = color[0] 143 | colors[c + 1] = color[1] 144 | colors[c + 2] = color[2] 145 | if (colorSize > 3) { 146 | colors[c + 3] = color[3] 147 | } 148 | c += colorSize 149 | } 150 | } 151 | return new THREE.Float32BufferAttribute(colors, colorSize) 152 | } 153 | 154 | private setMatricesFromVimx ( 155 | three: THREE.InstancedMesh, 156 | source: G3dScene | G3d, 157 | instances: number[] 158 | ) { 159 | const matrix = new THREE.Matrix4() 160 | for (let i = 0; i < instances.length; i++) { 161 | const array = source.getInstanceMatrix(instances[i]) 162 | matrix.fromArray(array) 163 | three.setMatrixAt(i, matrix) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/instancedSubmesh.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { Submesh } from '../mesh' 6 | import { InstancedMesh } from './instancedMesh' 7 | 8 | export class InstancedSubmesh { 9 | mesh: InstancedMesh 10 | index: number 11 | 12 | constructor (mesh: InstancedMesh, index: number) { 13 | this.mesh = mesh 14 | this.index = index 15 | } 16 | 17 | equals (other: Submesh) { 18 | return this.mesh === other.mesh && this.index === other.index 19 | } 20 | 21 | /** 22 | * Returns parent three mesh. 23 | */ 24 | get three () { 25 | return this.mesh.mesh 26 | } 27 | 28 | /** 29 | * True if parent mesh is merged. 30 | */ 31 | get merged () { 32 | return false 33 | } 34 | 35 | /** 36 | * Returns vim instance associated with this submesh. 37 | */ 38 | get instance () { 39 | return this.mesh.bimInstances[this.index] 40 | } 41 | 42 | /** 43 | * Returns bounding box for this submesh. 44 | */ 45 | get boundingBox () { 46 | return this.mesh.boxes[this.index] 47 | } 48 | 49 | /** 50 | * Returns vim object for this submesh. 51 | */ 52 | get object () { 53 | return this.mesh.vim.getObjectFromInstance(this.instance) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/legacyMeshFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { InsertableMesh } from './insertableMesh' 6 | import { Scene } from '../../vim-loader/scene' 7 | import { G3dMaterial, G3d, MeshSection } from 'vim-format' 8 | import { InstancedMeshFactory } from './instancedMeshFactory' 9 | import { G3dSubset } from './g3dSubset' 10 | 11 | /** 12 | * Mesh factory to load a standard vim using the progressive pipeline. 13 | */ 14 | export class VimMeshFactory { 15 | readonly g3d: G3d 16 | private _materials: G3dMaterial 17 | private _instancedFactory: InstancedMeshFactory 18 | private _scene: Scene 19 | 20 | constructor (g3d: G3d, materials: G3dMaterial, scene: Scene) { 21 | this.g3d = g3d 22 | this._materials = materials 23 | this._scene = scene 24 | this._instancedFactory = new InstancedMeshFactory(materials) 25 | } 26 | 27 | /** 28 | * Adds all instances from subset to the scene 29 | */ 30 | public add (subset: G3dSubset) { 31 | const uniques = subset.filterUniqueMeshes() 32 | const nonUniques = subset.filterNonUniqueMeshes() 33 | 34 | // Create and add meshes to scene 35 | this.addInstancedMeshes(this._scene, nonUniques) 36 | this.addMergedMesh(this._scene, uniques) 37 | } 38 | 39 | private addMergedMesh (scene: Scene, subset: G3dSubset) { 40 | const opaque = this.createMergedMesh(subset, 'opaque', false) 41 | const transparents = this.createMergedMesh(subset, 'transparent', true) 42 | scene.addMesh(opaque) 43 | scene.addMesh(transparents) 44 | } 45 | 46 | private createMergedMesh ( 47 | subset: G3dSubset, 48 | section: MeshSection, 49 | transparent: boolean 50 | ) { 51 | const offsets = subset.getOffsets(section) 52 | const opaque = new InsertableMesh(offsets, this._materials, transparent) 53 | 54 | const count = subset.getMeshCount() 55 | for (let m = 0; m < count; m++) { 56 | opaque.insertFromVim(this.g3d, m) 57 | } 58 | 59 | opaque.update() 60 | return opaque 61 | } 62 | 63 | private addInstancedMeshes (scene: Scene, subset: G3dSubset) { 64 | const count2 = subset.getMeshCount() 65 | for (let m = 0; m < count2; m++) { 66 | const mesh = subset.getSourceMesh(m) 67 | const instances = 68 | subset.getMeshInstances(m) ?? this.g3d.meshInstances[mesh] 69 | 70 | const opaque = this._instancedFactory.createOpaqueFromVim( 71 | this.g3d, 72 | mesh, 73 | instances 74 | ) 75 | const transparent = this._instancedFactory.createTransparentFromVim( 76 | this.g3d, 77 | mesh, 78 | instances 79 | ) 80 | scene.addMesh(opaque) 81 | scene.addMesh(transparent) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/loadingSynchronizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { G3dMesh } from 'vim-format' 6 | import { G3dSubset } from './g3dSubset' 7 | 8 | /** 9 | * Makes sure both instanced meshes and merged meshes are requested in the right order 10 | * Also decouples downloads and processing. 11 | */ 12 | export class LoadingSynchronizer { 13 | done = false 14 | uniques: G3dSubset 15 | nonUniques: G3dSubset 16 | getMesh: (mesh: number) => Promise 17 | mergeAction: (mesh: G3dMesh, index: number) => void 18 | instanceAction: (mesh: G3dMesh, index: number) => void 19 | 20 | mergeQueue: (() => void)[] = [] 21 | instanceQueue: (() => void)[] = [] 22 | 23 | constructor ( 24 | uniques: G3dSubset, 25 | nonUniques: G3dSubset, 26 | getMesh: (mesh: number) => Promise, 27 | mergeAction: (mesh: G3dMesh, index: number) => void, 28 | instanceAction: (mesh: G3dMesh, index: number) => void 29 | ) { 30 | this.uniques = uniques 31 | this.nonUniques = nonUniques 32 | this.getMesh = getMesh 33 | this.mergeAction = mergeAction 34 | this.instanceAction = instanceAction 35 | } 36 | 37 | get isDone () { 38 | return this.done 39 | } 40 | 41 | abort () { 42 | this.done = true 43 | this.mergeQueue.length = 0 44 | this.instanceQueue.length = 0 45 | } 46 | 47 | // Loads batches until the all meshes are loaded 48 | async loadAll () { 49 | const promises = this.getSortedPromises() 50 | Promise.all(promises).then(() => (this.done = true)) 51 | await this.consumeQueues() 52 | } 53 | 54 | private async consumeQueues () { 55 | while ( 56 | !( 57 | this.done && 58 | this.mergeQueue.length === 0 && 59 | this.instanceQueue.length === 0 60 | ) 61 | ) { 62 | while (this.mergeQueue.length > 0) { 63 | this.mergeQueue.pop()() 64 | } 65 | while (this.instanceQueue.length > 0) { 66 | this.instanceQueue.pop()() 67 | } 68 | 69 | // Resume on next frame 70 | await new Promise((resolve) => setTimeout(resolve, 0)) 71 | } 72 | } 73 | 74 | private getSortedPromises () { 75 | const promises: Promise[] = [] 76 | 77 | const uniqueCount = this.uniques.getMeshCount() 78 | const nonUniquesCount = this.nonUniques.getMeshCount() 79 | 80 | let uniqueIndex = 0 81 | let nonUniqueIndex = 0 82 | let uniqueMesh = 0 83 | let nonUniqueMesh = 0 84 | 85 | while (!this.isDone) { 86 | const mergeDone = uniqueIndex >= uniqueCount 87 | const instanceDone = nonUniqueIndex >= nonUniquesCount 88 | if (mergeDone && instanceDone) { 89 | break 90 | } 91 | 92 | if (!mergeDone && (uniqueMesh <= nonUniqueMesh || instanceDone)) { 93 | uniqueMesh = this.uniques.getSourceMesh(uniqueIndex) 94 | promises.push(this.merge(uniqueMesh, uniqueIndex++)) 95 | } 96 | if (!instanceDone && (nonUniqueMesh <= uniqueMesh || mergeDone)) { 97 | nonUniqueMesh = this.nonUniques.getSourceMesh(nonUniqueIndex) 98 | promises.push(this.instance(nonUniqueMesh, nonUniqueIndex++)) 99 | } 100 | } 101 | return promises 102 | } 103 | 104 | async merge (mesh: number, index: number) { 105 | const m = await this.getMesh(mesh) 106 | this.mergeQueue.push(() => this.mergeAction(m, index)) 107 | } 108 | 109 | async instance (mesh: number, index: number) { 110 | const m = await this.getMesh(mesh) 111 | this.instanceQueue.push(() => this.instanceAction(m, index)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/open.ts: -------------------------------------------------------------------------------- 1 | // loader 2 | import { 3 | getFullSettings, 4 | VimPartialSettings, 5 | VimSettings 6 | } from '../vimSettings' 7 | 8 | import { Vim } from '../vim' 9 | import { Scene } from '../scene' 10 | import { Vimx } from './vimx' 11 | 12 | import { VimSource } from '../../index' 13 | import { ElementMapping, ElementMapping2 } from '../elementMapping' 14 | import { 15 | BFast, 16 | RemoteBuffer, 17 | RemoteVimx, 18 | requestHeader, 19 | IProgressLogs, 20 | VimDocument, 21 | G3d, 22 | G3dMaterial 23 | } from 'vim-format' 24 | import { VimSubsetBuilder, VimxSubsetBuilder } from './subsetBuilder' 25 | import { VimMeshFactory } from './legacyMeshFactory' 26 | import { DefaultLog } from 'vim-format/dist/logging' 27 | 28 | /** 29 | * Asynchronously opens a vim object from a given source with the provided settings. 30 | * @param {string | BFast} source - The source of the vim object, either a string or a BFast. 31 | * @param {VimPartialSettings} settings - The settings to configure the behavior of the vim object. 32 | * @param {(p: IProgressLogs) => void} [onProgress] - Optional callback function to track progress logs. 33 | * @returns {Promise} A Promise that resolves when the vim object is successfully opened. 34 | */ 35 | export async function open ( 36 | source: VimSource | BFast, 37 | settings: VimPartialSettings, 38 | onProgress?: (p: IProgressLogs) => void 39 | ) { 40 | const bfast = source instanceof BFast ? source : new BFast(source) 41 | const fullSettings = getFullSettings(settings) 42 | const type = await determineFileType(bfast, fullSettings)! 43 | 44 | if (type === 'vim') { 45 | return loadFromVim(bfast, fullSettings, onProgress) 46 | } 47 | 48 | if (type === 'vimx') { 49 | return loadFromVimX(bfast, fullSettings, onProgress) 50 | } 51 | 52 | throw new Error('Cannot determine the appropriate loading strategy.') 53 | } 54 | 55 | async function determineFileType ( 56 | bfast: BFast, 57 | settings: VimSettings 58 | ) { 59 | if (settings?.fileType === 'vim') return 'vim' 60 | if (settings?.fileType === 'vimx') return 'vimx' 61 | return requestFileType(bfast) 62 | } 63 | 64 | async function requestFileType (bfast: BFast) { 65 | if (bfast.url) { 66 | if (bfast.url.endsWith('vim')) return 'vim' 67 | if (bfast.url.endsWith('vimx')) return 'vimx' 68 | } 69 | 70 | const header = await requestHeader(bfast) 71 | if (header.vim !== undefined) return 'vim' 72 | if (header.vimx !== undefined) return 'vimx' 73 | 74 | throw new Error('Cannot determine file type from header.') 75 | } 76 | 77 | /** 78 | * Loads a Vimx file from source 79 | */ 80 | async function loadFromVimX ( 81 | bfast: BFast, 82 | settings: VimSettings, 83 | onProgress: (p: IProgressLogs) => void 84 | ) { 85 | // Fetch geometry data 86 | const remoteVimx = new RemoteVimx(bfast) 87 | if (remoteVimx.bfast.source instanceof RemoteBuffer) { 88 | remoteVimx.bfast.source.onProgress = onProgress 89 | } 90 | 91 | const vimx = await Vimx.fromRemote(remoteVimx, !settings.progressive) 92 | 93 | // Create scene 94 | const scene = new Scene(settings.matrix) 95 | const mapping = new ElementMapping2(vimx.scene) 96 | 97 | // wait for bim data. 98 | // const bim = bimPromise ? await bimPromise : undefined 99 | 100 | const builder = new VimxSubsetBuilder(vimx, scene) 101 | 102 | const vim = new Vim( 103 | vimx.header, 104 | undefined, 105 | undefined, 106 | scene, 107 | settings, 108 | mapping, 109 | builder, 110 | bfast.url, 111 | 'vimx' 112 | ) 113 | 114 | if (remoteVimx.bfast.source instanceof RemoteBuffer) { 115 | remoteVimx.bfast.source.onProgress = undefined 116 | } 117 | 118 | return vim 119 | } 120 | 121 | /** 122 | * Loads a Vim file from source 123 | */ 124 | async function loadFromVim ( 125 | bfast: BFast, 126 | settings: VimSettings, 127 | onProgress?: (p: IProgressLogs) => void 128 | ) { 129 | const fullSettings = getFullSettings(settings) 130 | 131 | if (bfast.source instanceof RemoteBuffer) { 132 | bfast.source.onProgress = onProgress 133 | if (settings.verboseHttp) { 134 | bfast.source.logs = new DefaultLog() 135 | } 136 | } 137 | 138 | // Fetch g3d data 139 | const geometry = await bfast.getBfast('geometry') 140 | const g3d = await G3d.createFromBfast(geometry) 141 | const materials = new G3dMaterial(g3d.materialColors) 142 | 143 | // Create scene 144 | const scene = new Scene(settings.matrix) 145 | const factory = new VimMeshFactory(g3d, materials, scene) 146 | 147 | // Create legacy mapping 148 | const doc = await VimDocument.createFromBfast(bfast) 149 | const mapping = await ElementMapping.fromG3d(g3d, doc) 150 | const header = await requestHeader(bfast) 151 | 152 | // Return legacy vim 153 | const builder = new VimSubsetBuilder(factory) 154 | const vim = new Vim( 155 | header, 156 | doc, 157 | g3d, 158 | scene, 159 | fullSettings, 160 | mapping, 161 | builder, 162 | bfast.url, 163 | 'vim' 164 | ) 165 | 166 | if (bfast.source instanceof RemoteBuffer) { 167 | bfast.source.onProgress = undefined 168 | } 169 | 170 | return vim 171 | } 172 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/subsetBuilder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { VimMeshFactory } from './legacyMeshFactory' 6 | import { LoadPartialSettings, LoadSettings, SubsetRequest } from './subsetRequest' 7 | import { G3dSubset } from './g3dSubset' 8 | import { ISignal, ISignalHandler, SignalDispatcher } from 'ste-signals' 9 | import { ISubscribable, SubscriptionChangeEventHandler } from 'ste-core' 10 | import { Vimx } from './vimx' 11 | import { Scene } from '../scene' 12 | 13 | export interface SubsetBuilder { 14 | /** Dispatched whenever a subset begins or finishes loading. */ 15 | onUpdate: ISignal 16 | 17 | /** Returns true when some subset is being loaded. */ 18 | isLoading: boolean 19 | 20 | /** Returns all instances as a subset */ 21 | getFullSet(): G3dSubset 22 | 23 | /** Loads given subset with given options */ 24 | loadSubset(subset: G3dSubset, settings?: LoadPartialSettings) 25 | 26 | /** Stops and clears all loading processes */ 27 | clear() 28 | 29 | dispose() 30 | } 31 | 32 | /** 33 | * Loads and builds subsets from a Vim file. 34 | */ 35 | export class VimSubsetBuilder implements SubsetBuilder { 36 | factory: VimMeshFactory 37 | 38 | private _onUpdate = new SignalDispatcher() 39 | 40 | get onUpdate () { 41 | return this._onUpdate.asEvent() 42 | } 43 | 44 | get isLoading () { 45 | return false 46 | } 47 | 48 | constructor (factory: VimMeshFactory) { 49 | this.factory = factory 50 | } 51 | 52 | getFullSet () { 53 | return new G3dSubset(this.factory.g3d) 54 | } 55 | 56 | loadSubset (subset: G3dSubset, settings?: LoadPartialSettings) { 57 | this.factory.add(subset) 58 | this._onUpdate.dispatch() 59 | } 60 | 61 | clear () { 62 | this._onUpdate.dispatch() 63 | } 64 | 65 | dispose () {} 66 | } 67 | 68 | /** 69 | * Loads and builds subsets from a VimX file. 70 | */ 71 | export class VimxSubsetBuilder { 72 | private _localVimx: Vimx 73 | private _scene: Scene 74 | private _set = new Set() 75 | 76 | private _onUpdate = new SignalDispatcher() 77 | get onUpdate () { 78 | return this._onUpdate.asEvent() 79 | } 80 | 81 | get isLoading () { 82 | return this._set.size > 0 83 | } 84 | 85 | constructor (localVimx: Vimx, scene: Scene) { 86 | this._localVimx = localVimx 87 | this._scene = scene 88 | } 89 | 90 | getFullSet () { 91 | return new G3dSubset(this._localVimx.scene) 92 | } 93 | 94 | async loadSubset (subset: G3dSubset, settings?: LoadPartialSettings) { 95 | const request = new SubsetRequest(this._scene, this._localVimx, subset) 96 | this._set.add(request) 97 | this._onUpdate.dispatch() 98 | await request.start(settings) 99 | this._set.delete(request) 100 | this._onUpdate.dispatch() 101 | } 102 | 103 | clear () { 104 | this._localVimx.abort() 105 | this._set.forEach((s) => s.dispose()) 106 | this._set.clear() 107 | this._onUpdate.dispatch() 108 | } 109 | 110 | dispose () { 111 | this.clear() 112 | } 113 | } 114 | 115 | export class DummySubsetBuilder implements SubsetBuilder { 116 | get onUpdate () { 117 | return new AlwaysTrueSignal() 118 | } 119 | 120 | get isLoading () { 121 | return false 122 | } 123 | 124 | getFullSet (): G3dSubset { 125 | throw new Error('Method not implemented.') 126 | } 127 | 128 | loadSubset (subset: G3dSubset, settings?: Partial) {} 129 | clear () { } 130 | dispose () { } 131 | } 132 | 133 | class AlwaysTrueSignal implements ISignal { 134 | count: number 135 | subscribe (fn: ISignalHandler): () => void { 136 | fn(null) 137 | return () => {} 138 | } 139 | 140 | sub (fn: ISignalHandler): () => void { 141 | fn(null) 142 | return () => {} 143 | } 144 | 145 | unsubscribe (fn: ISignalHandler): void {} 146 | unsub (fn: ISignalHandler): void {} 147 | one (fn: ISignalHandler): () => void { 148 | fn(null) 149 | return () => {} 150 | } 151 | 152 | has (fn: ISignalHandler): boolean { 153 | return false 154 | } 155 | 156 | clear (): void {} 157 | onSubscriptionChange: ISubscribable 158 | } 159 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/subsetRequest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { InsertableMesh } from './insertableMesh' 6 | import { InstancedMeshFactory } from './instancedMeshFactory' 7 | import { Vimx, Scene } from '../..' 8 | 9 | import { G3dMesh } from 'vim-format' 10 | import { G3dSubset } from './g3dSubset' 11 | import { InstancedMesh } from './instancedMesh' 12 | import { LoadingSynchronizer } from './loadingSynchronizer' 13 | 14 | export type LoadSettings = { 15 | /** Delay in ms between each rendering list update. @default: 400ms */ 16 | updateDelayMs: number 17 | /** If true, will wait for geometry to be ready before it is added to the renderer. @default: false */ 18 | delayRender: boolean 19 | } 20 | 21 | export type LoadPartialSettings = Partial 22 | function getFullSettings (option: LoadPartialSettings) { 23 | return { 24 | updateDelayMs: option?.updateDelayMs ?? 400, 25 | delayRender: option?.delayRender ?? false 26 | } as LoadSettings 27 | } 28 | 29 | /** 30 | * Manages geometry downloads and loads it into a scene for rendering. 31 | */ 32 | export class SubsetRequest { 33 | private _subset: G3dSubset 34 | 35 | private _uniques: G3dSubset 36 | private _nonUniques: G3dSubset 37 | private _opaqueMesh: InsertableMesh 38 | private _transparentMesh: InsertableMesh 39 | 40 | private _synchronizer: LoadingSynchronizer 41 | private _meshFactory: InstancedMeshFactory 42 | private _meshes: InstancedMesh[] = [] 43 | private _pushedMesh = 0 44 | 45 | private _disposed: boolean = false 46 | private _started: boolean = false 47 | 48 | private _scene: Scene 49 | 50 | getBoundingBox () { 51 | return this._subset.getBoundingBox() 52 | } 53 | 54 | constructor (scene: Scene, localVimx: Vimx, subset: G3dSubset) { 55 | this._subset = subset 56 | this._scene = scene 57 | 58 | this._uniques = this._subset.filterUniqueMeshes() 59 | this._nonUniques = this._subset.filterNonUniqueMeshes() 60 | 61 | const opaqueOffsets = this._uniques.getOffsets('opaque') 62 | this._opaqueMesh = new InsertableMesh( 63 | opaqueOffsets, 64 | localVimx.materials, 65 | false 66 | ) 67 | this._opaqueMesh.mesh.name = 'Opaque_Merged_Mesh' 68 | 69 | const transparentOffsets = this._uniques.getOffsets('transparent') 70 | this._transparentMesh = new InsertableMesh( 71 | transparentOffsets, 72 | localVimx.materials, 73 | true 74 | ) 75 | this._transparentMesh.mesh.name = 'Transparent_Merged_Mesh' 76 | 77 | this._scene.addMesh(this._transparentMesh) 78 | this._scene.addMesh(this._opaqueMesh) 79 | 80 | this._meshFactory = new InstancedMeshFactory(localVimx.materials) 81 | 82 | this._synchronizer = new LoadingSynchronizer( 83 | this._uniques, 84 | this._nonUniques, 85 | (mesh) => localVimx.getMesh(mesh), 86 | (mesh, index) => this.mergeMesh(mesh, index), 87 | (mesh, index) => 88 | this.instanceMesh(mesh, this._nonUniques.getMeshInstances(index)) 89 | ) 90 | 91 | return this 92 | } 93 | 94 | dispose () { 95 | if (!this._disposed) { 96 | this._disposed = true 97 | this._synchronizer.abort() 98 | } 99 | } 100 | 101 | async start (settings: LoadPartialSettings) { 102 | if (this._started) { 103 | return 104 | } 105 | this._started = true 106 | const fullSettings = getFullSettings(settings) 107 | 108 | // Loading and updates are independants 109 | this._synchronizer.loadAll() 110 | 111 | // Loop until done or disposed. 112 | let lastUpdate = Date.now() 113 | while (true) { 114 | await this.nextFrame() 115 | if (this._disposed) { 116 | return 117 | } 118 | if (this._synchronizer.isDone) { 119 | this.updateMeshes() 120 | return 121 | } 122 | if ( 123 | !fullSettings.delayRender && 124 | Date.now() - lastUpdate > fullSettings.updateDelayMs 125 | ) { 126 | this.updateMeshes() 127 | lastUpdate = Date.now() 128 | } 129 | } 130 | } 131 | 132 | private async nextFrame () { 133 | return new Promise((resolve) => setTimeout(resolve, 0)) 134 | } 135 | 136 | private mergeMesh (g3dMesh: G3dMesh, index: number) { 137 | this._transparentMesh.insert(g3dMesh, index) 138 | this._opaqueMesh.insert(g3dMesh, index) 139 | } 140 | 141 | private instanceMesh (g3dMesh: G3dMesh, instances: number[]) { 142 | const opaque = this._meshFactory.createOpaque(g3dMesh, instances) 143 | const transparent = this._meshFactory.createTransparent(g3dMesh, instances) 144 | 145 | if (opaque) { 146 | this._meshes.push(opaque) 147 | } 148 | if (transparent) { 149 | this._meshes.push(transparent) 150 | } 151 | } 152 | 153 | private updateMeshes () { 154 | // Update Instanced meshes 155 | while (this._pushedMesh < this._meshes.length) { 156 | const mesh = this._meshes[this._pushedMesh++] 157 | this._scene.addMesh(mesh) 158 | } 159 | 160 | // Update Merged meshes 161 | this._transparentMesh.update() 162 | this._opaqueMesh.update() 163 | this._scene.setDirty() 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/vimRequest.ts: -------------------------------------------------------------------------------- 1 | // loader 2 | import { 3 | VimPartialSettings 4 | } from '../vimSettings' 5 | 6 | import { Vim } from '../vim' 7 | import { DeferredPromise } from '../../utils/deferredPromise' 8 | import { RequestResult, ErrorResult, SuccessResult } from '../../utils/requestResult' 9 | import { open } from './open' 10 | 11 | import { VimSource } from '../../index' 12 | import { 13 | BFast, IProgressLogs 14 | } from 'vim-format' 15 | 16 | export type RequestSource = { 17 | url?: string, 18 | buffer?: ArrayBuffer, 19 | headers?: Record, 20 | } 21 | 22 | /** 23 | * Initiates a request to load a VIM object from a given source. 24 | * @param options a url where to find the vim file or a buffer of a vim file. 25 | * @param settings the settings to configure how the vim will be loaded. 26 | * @returns a request object that can be used to track progress and get the result. 27 | */ 28 | export function request (options: RequestSource, settings? : VimPartialSettings) { 29 | return new VimRequest(options, settings) 30 | } 31 | 32 | /** 33 | * A class that represents a request to load a VIM object from a given source. 34 | */ 35 | export class VimRequest { 36 | private _source: VimSource 37 | private _settings : VimPartialSettings 38 | private _bfast : BFast 39 | 40 | // Result states 41 | private _isDone: boolean = false 42 | private _vimResult?: Vim 43 | private _error?: string 44 | 45 | // Promises to await progress updates and completion 46 | private _progress : IProgressLogs = { loaded: 0, total: 0, all: new Map() } 47 | private _progressPromise = new DeferredPromise() 48 | private _completionPromise = new DeferredPromise() 49 | 50 | constructor (source: VimSource, settings: VimPartialSettings) { 51 | this._source = source 52 | this._settings = settings 53 | 54 | this.startRequest() 55 | } 56 | 57 | /** 58 | * Initiates the asynchronous request and handles progress updates. 59 | */ 60 | private async startRequest () { 61 | try { 62 | this._bfast = new BFast(this._source) 63 | 64 | const vim: Vim = await open(this._bfast, this._settings, (progress: IProgressLogs) => { 65 | this._progress = progress 66 | this._progressPromise.resolve(progress) 67 | this._progressPromise = new DeferredPromise() 68 | }) 69 | this._vimResult = vim 70 | } catch (err: any) { 71 | this._error = err.message ?? JSON.stringify(err) 72 | console.error('Error loading VIM:', err) 73 | } finally { 74 | this.end() 75 | } 76 | } 77 | 78 | private end () { 79 | this._isDone = true 80 | this._progressPromise.resolve(this._progress) 81 | this._completionPromise.resolve() 82 | } 83 | 84 | async getResult (): Promise> { 85 | await this._completionPromise 86 | return this._error ? new ErrorResult(this._error) : new SuccessResult(this._vimResult) 87 | } 88 | 89 | /** 90 | * Async generator that yields progress updates. 91 | * @returns An AsyncGenerator yielding IProgressLogs. 92 | */ 93 | async * getProgress (): AsyncGenerator { 94 | while (!this._isDone) { 95 | yield await this._progressPromise 96 | } 97 | } 98 | 99 | abort () { 100 | this._bfast.abort() 101 | this._error = 'Request aborted' 102 | this.end() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/vim-loader/progressive/vimx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import { G3dMaterial, RemoteVimx, VimHeader, G3dScene } from 'vim-format' 6 | 7 | /** 8 | * Interface to interact with a vimx 9 | */ 10 | export class Vimx { 11 | private readonly vimx: RemoteVimx 12 | readonly scene: G3dScene 13 | readonly materials: G3dMaterial 14 | readonly header: VimHeader 15 | 16 | static async fromRemote (vimx: RemoteVimx, downloadMeshes: boolean) { 17 | if (downloadMeshes) { 18 | await vimx.bfast.forceDownload() 19 | } 20 | const [header, scene, materials] = await Promise.all([ 21 | await vimx.getHeader(), 22 | await vimx.getScene(), 23 | await vimx.getMaterials() 24 | ]) 25 | 26 | return new Vimx(vimx, header, scene, materials) 27 | } 28 | 29 | private constructor ( 30 | vimx: RemoteVimx, 31 | header: VimHeader, 32 | scene: G3dScene, 33 | material: G3dMaterial 34 | ) { 35 | this.vimx = vimx 36 | this.header = header 37 | this.scene = scene 38 | this.materials = material 39 | } 40 | 41 | getMesh (mesh: number) { 42 | return this.vimx.getMesh(mesh) 43 | } 44 | 45 | abort () { 46 | this.vimx.abort() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/vim-loader/vimSettings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vim-loader 3 | */ 4 | 5 | import deepmerge from 'deepmerge' 6 | import { Transparency } from './geometry' 7 | import * as THREE from 'three' 8 | 9 | export type FileType = 'vim' | 'vimx' | undefined 10 | 11 | /** 12 | * Represents settings for configuring the behavior and rendering of a vim object. 13 | */ 14 | export type VimSettings = { 15 | 16 | /** 17 | * The positional offset for the vim object. 18 | */ 19 | position: THREE.Vector3 20 | 21 | /** 22 | * The XYZ rotation applied to the vim object. 23 | */ 24 | rotation: THREE.Vector3 25 | 26 | /** 27 | * The scaling factor applied to the vim object. 28 | */ 29 | scale: number 30 | 31 | /** 32 | * The matrix representation of the vim object's position, rotation, and scale. 33 | * Setting this will override individual position, rotation, and scale properties. 34 | */ 35 | matrix: THREE.Matrix4 36 | 37 | /** 38 | * Determines whether objects are drawn based on their transparency. 39 | */ 40 | transparency: Transparency.Mode 41 | 42 | /** 43 | * Set to true to enable verbose HTTP logging. 44 | */ 45 | verboseHttp: boolean 46 | 47 | // VIMX 48 | 49 | /** 50 | * Specifies the file type (vim or vimx) if it cannot or should not be inferred from the file extension. 51 | */ 52 | fileType: FileType 53 | 54 | /** 55 | * Set to true to stream geometry to the scene. Only supported with vimx files. 56 | */ 57 | progressive: boolean 58 | 59 | /** 60 | * The time in milliseconds between each scene refresh during progressive loading. 61 | */ 62 | progressiveInterval: number 63 | } 64 | 65 | /** 66 | * Default configuration settings for a vim object. 67 | */ 68 | export const defaultConfig: VimSettings = { 69 | position: new THREE.Vector3(), 70 | rotation: new THREE.Vector3(), 71 | scale: 1, 72 | matrix: undefined, 73 | transparency: 'all', 74 | verboseHttp: false, 75 | 76 | // progressive 77 | fileType: undefined, 78 | progressive: false, 79 | progressiveInterval: 1000 80 | } 81 | 82 | /** 83 | * Represents a partial configuration of settings for a vim object. 84 | */ 85 | export type VimPartialSettings = Partial 86 | 87 | /** 88 | * Wraps Vim options, converting values to related THREE.js types and providing default values. 89 | * @param {VimPartialSettings} [options] - Optional partial settings for the Vim object. 90 | * @returns {VimSettings} The complete settings for the Vim object, including defaults. 91 | */ 92 | export function getFullSettings (options?: VimPartialSettings) { 93 | const merge = options 94 | ? deepmerge(defaultConfig, options, undefined) 95 | : defaultConfig 96 | 97 | merge.transparency = Transparency.isValid(merge.transparency!) 98 | ? merge.transparency 99 | : 'all' 100 | 101 | merge.matrix = merge.matrix ?? new THREE.Matrix4().compose( 102 | merge.position, 103 | new THREE.Quaternion().setFromEuler( 104 | new THREE.Euler( 105 | (merge.rotation.x * Math.PI) / 180, 106 | (merge.rotation.y * Math.PI) / 180, 107 | (merge.rotation.z * Math.PI) / 180 108 | ) 109 | ), 110 | new THREE.Vector3(merge.scale, merge.scale, merge.scale) 111 | ) 112 | 113 | return merge 114 | } 115 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/camera/cameraMovementLerp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/camera 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { Camera } from './camera' 7 | import { Object3D } from '../../vim-loader/object3D' 8 | import { CameraMovementSnap } from './cameraMovementSnap' 9 | import { CameraMovement } from './cameraMovement' 10 | 11 | export class CameraLerp extends CameraMovement { 12 | _movement: CameraMovementSnap 13 | _clock = new THREE.Clock() 14 | 15 | // position 16 | onProgress: ((progress: number) => void) | undefined 17 | 18 | _duration = 1 19 | 20 | constructor (camera: Camera, movement: CameraMovementSnap) { 21 | super(camera) 22 | this._movement = movement 23 | } 24 | 25 | get isLerping () { 26 | return this._clock.running 27 | } 28 | 29 | init (duration: number) { 30 | this.cancel() 31 | this._duration = duration 32 | this._clock.start() 33 | } 34 | 35 | cancel () { 36 | this._clock.stop() 37 | this.onProgress = undefined 38 | } 39 | 40 | easeOutCubic (x: number): number { 41 | return 1 - Math.pow(1 - x, 3) 42 | } 43 | 44 | update () { 45 | if (!this._clock.running) return 46 | 47 | let t = this._clock.getElapsedTime() / this._duration 48 | t = this.easeOutCubic(t) 49 | if (t >= 1) { 50 | t = 1 51 | this._clock.stop() 52 | this.onProgress = undefined 53 | } 54 | this.onProgress?.(t) 55 | } 56 | 57 | override move3 (vector: THREE.Vector3): void { 58 | const v = vector.clone() 59 | v.applyQuaternion(this._camera.quaternion) 60 | const start = this._camera.position.clone() 61 | const end = this._camera.position.clone().add(v) 62 | const pos = new THREE.Vector3() 63 | 64 | this.onProgress = (progress) => { 65 | pos.copy(start) 66 | pos.lerp(end, progress) 67 | this._movement.move3(pos) 68 | } 69 | } 70 | 71 | rotate (angle: THREE.Vector2): void { 72 | const euler = new THREE.Euler(0, 0, 0, 'YXZ') 73 | euler.setFromQuaternion(this._camera.quaternion) 74 | 75 | // When moving the mouse one full sreen 76 | // Orbit will rotate 180 degree around the scene 77 | euler.x += angle.x 78 | euler.y += angle.y 79 | euler.z = 0 80 | 81 | // Clamp X rotation to prevent performing a loop. 82 | const max = Math.PI * 0.48 83 | euler.x = Math.max(-max, Math.min(max, euler.x)) 84 | 85 | const start = this._camera.quaternion.clone() 86 | const end = new THREE.Quaternion().setFromEuler(euler) 87 | const rot = new THREE.Quaternion() 88 | this.onProgress = (progress) => { 89 | rot.copy(start) 90 | rot.slerp(end, progress) 91 | this._movement.applyRotation(rot) 92 | } 93 | } 94 | 95 | zoom (amount: number): void { 96 | const dist = this._camera.orbitDistance * amount 97 | this.setDistance(dist) 98 | } 99 | 100 | setDistance (dist: number): void { 101 | const start = this._camera.position.clone() 102 | const end = this._camera.target 103 | .clone() 104 | .lerp(start, dist / this._camera.orbitDistance) 105 | 106 | this.onProgress = (progress) => { 107 | this._camera.position.copy(start) 108 | this._camera.position.lerp(end, progress) 109 | } 110 | } 111 | 112 | orbit (angle: THREE.Vector2): void { 113 | const startPos = this._camera.position.clone() 114 | const startTarget = this._camera.target.clone() 115 | const a = new THREE.Vector2() 116 | 117 | this.onProgress = (progress) => { 118 | a.set(0, 0) 119 | a.lerp(angle, progress) 120 | this._movement.set(startPos, startTarget) 121 | this._movement.orbit(a) 122 | } 123 | } 124 | 125 | target (target: Object3D | THREE.Vector3): void { 126 | const pos = target instanceof Object3D ? target.getCenter() : target 127 | const next = pos.clone().sub(this._camera.position) 128 | const start = this._camera.quaternion.clone() 129 | const rot = new THREE.Quaternion().setFromUnitVectors( 130 | new THREE.Vector3(0, 0, -1), 131 | next.normalize() 132 | ) 133 | this.onProgress = (progress) => { 134 | const r = start.clone().slerp(rot, progress) 135 | this._movement.applyRotation(r) 136 | } 137 | } 138 | 139 | reset (): void { 140 | this.set(this._camera._savedPosition, this._camera._savedTarget) 141 | } 142 | 143 | set (position: THREE.Vector3, target?: THREE.Vector3) { 144 | const endTarget = target ?? this._camera.target 145 | const startPos = this._camera.position.clone() 146 | const startTarget = this._camera.target.clone() 147 | this.onProgress = (progress) => { 148 | this._movement.set( 149 | startPos.clone().lerp(position, progress), 150 | startTarget.clone().lerp(endTarget, progress) 151 | ) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/camera/cameraMovementSnap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/camera 3 | */ 4 | 5 | import { CameraMovement } from './cameraMovement' 6 | import { Object3D } from '../../vim-loader/object3D' 7 | import * as THREE from 'three' 8 | 9 | export class CameraMovementSnap extends CameraMovement { 10 | /** 11 | * Moves the camera closer or farther away from orbit target. 12 | * @param amount movement size. 13 | */ 14 | zoom (amount: number): void { 15 | const dist = this._camera.orbitDistance * amount 16 | this.setDistance(dist) 17 | } 18 | 19 | reset () { 20 | this.set(this._camera._savedPosition, this._camera._savedTarget) 21 | } 22 | 23 | setDistance (dist: number): void { 24 | const pos = this._camera.target 25 | .clone() 26 | .sub(this._camera.forward.multiplyScalar(dist)) 27 | 28 | this.set(pos, this._camera.target) 29 | } 30 | 31 | rotate (angle: THREE.Vector2): void { 32 | const locked = angle.clone().multiply(this._camera.allowedRotation) 33 | const rotation = this.predictRotate(locked) 34 | this.applyRotation(rotation) 35 | } 36 | 37 | applyRotation (quaternion: THREE.Quaternion) { 38 | this._camera.quaternion.copy(quaternion) 39 | const target = this._camera.forward 40 | .multiplyScalar(this._camera.orbitDistance) 41 | .add(this._camera.position) 42 | 43 | this.set(this._camera.position, target) 44 | } 45 | 46 | target (target: Object3D | THREE.Vector3): void { 47 | const pos = target instanceof Object3D ? target.getCenter() : target 48 | if (!pos) return 49 | this.set(this._camera.position, pos) 50 | } 51 | 52 | orbit (angle: THREE.Vector2): void { 53 | const locked = angle.clone().multiply(this._camera.allowedRotation) 54 | const pos = this.predictOrbit(locked) 55 | this.set(pos) 56 | } 57 | 58 | override move3 (vector: THREE.Vector3): void { 59 | const v = vector.clone() 60 | v.applyQuaternion(this._camera.quaternion) 61 | const locked = this.lockVector(v, new THREE.Vector3()) 62 | const pos = this._camera.position.clone().add(locked) 63 | const target = this._camera.target.clone().add(locked) 64 | this.set(pos, target) 65 | } 66 | 67 | set (position: THREE.Vector3, target?: THREE.Vector3) { 68 | // apply position 69 | const locked = this.lockVector(position, this._camera.position) 70 | this._camera.position.copy(locked) 71 | 72 | // apply target and rotation 73 | target = target ?? this._camera.target 74 | this._camera.target.copy(target) 75 | this._camera.camPerspective.camera.lookAt(target) 76 | this._camera.camPerspective.camera.up.set(0, 1, 0) 77 | } 78 | 79 | private lockVector (position: THREE.Vector3, fallback: THREE.Vector3) { 80 | const x = this._camera.allowedMovement.x === 0 ? fallback.x : position.x 81 | const y = this._camera.allowedMovement.y === 0 ? fallback.y : position.y 82 | const z = this._camera.allowedMovement.z === 0 ? fallback.z : position.z 83 | 84 | return new THREE.Vector3(x, y, z) 85 | } 86 | 87 | predictOrbit (angle: THREE.Vector2) { 88 | const rotation = this.predictRotate(angle) 89 | 90 | const delta = new THREE.Vector3(0, 0, 1) 91 | .applyQuaternion(rotation) 92 | .multiplyScalar(this._camera.orbitDistance) 93 | 94 | return this._camera.target.clone().add(delta) 95 | } 96 | 97 | predictRotate (angle: THREE.Vector2) { 98 | const euler = new THREE.Euler(0, 0, 0, 'YXZ') 99 | euler.setFromQuaternion(this._camera.quaternion) 100 | 101 | euler.x += (angle.x * Math.PI) / 180 102 | euler.y += (angle.y * Math.PI) / 180 103 | euler.z = 0 104 | 105 | // Clamp X rotation to prevent performing a loop. 106 | const max = Math.PI * 0.4999 107 | euler.x = Math.max(-max, Math.min(max, euler.x)) 108 | 109 | const rotation = new THREE.Quaternion().setFromEuler(euler) 110 | return rotation 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/camera/orthographic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/camera 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { ViewerSettings } from '../settings/viewerSettings' 7 | 8 | export class OrthographicWrapper { 9 | camera: THREE.OrthographicCamera 10 | 11 | constructor (camera: THREE.OrthographicCamera) { 12 | this.camera = camera 13 | } 14 | 15 | frustrumSizeAt (point: THREE.Vector3) { 16 | return new THREE.Vector2( 17 | this.camera.right - this.camera.left, 18 | this.camera.top - this.camera.bottom 19 | ) 20 | } 21 | 22 | applySettings (settings: ViewerSettings) { 23 | this.camera.zoom = settings.camera.zoom 24 | this.camera.near = -settings.camera.far 25 | this.camera.far = settings.camera.far 26 | this.camera.updateProjectionMatrix() 27 | } 28 | 29 | updateProjection (size: THREE.Vector2, aspect: number) { 30 | const max = Math.max(size.x, size.y) 31 | this.camera.left = -max * aspect 32 | this.camera.right = max * aspect 33 | this.camera.top = max 34 | this.camera.bottom = -max 35 | 36 | this.camera.updateProjectionMatrix() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/camera/perspective.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/camera 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { ViewerSettings } from '../settings/viewerSettings' 7 | 8 | export class PerspectiveWrapper { 9 | camera: THREE.PerspectiveCamera 10 | 11 | constructor (camera: THREE.PerspectiveCamera) { 12 | this.camera = camera 13 | } 14 | 15 | applySettings (settings: ViewerSettings) { 16 | this.camera.fov = settings.camera.fov 17 | this.camera.zoom = settings.camera.zoom 18 | this.camera.near = settings.camera.near 19 | this.camera.far = settings.camera.far 20 | this.camera.updateProjectionMatrix() 21 | } 22 | 23 | updateProjection (aspect: number) { 24 | this.camera.aspect = aspect 25 | this.camera.updateProjectionMatrix() 26 | } 27 | 28 | frustrumSizeAt (point: THREE.Vector3) { 29 | const dist = this.camera.position.distanceTo(point) 30 | const size = 2 * dist * Math.tan((this.camera.fov / 2) * (Math.PI / 180)) 31 | return new THREE.Vector2(size * this.camera.aspect, size) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/environment/cameraLight.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { ICamera } from '../camera/camera' 7 | 8 | export class CameraLight { 9 | readonly light : THREE.DirectionalLight 10 | private readonly _camera : ICamera 11 | private _unsubscribe : (() => void) | undefined = undefined 12 | 13 | /** 14 | * The position of the light. 15 | */ 16 | position: THREE.Vector3 17 | 18 | /** 19 | * The color of the light. 20 | */ 21 | get color () { 22 | return this.light.color 23 | } 24 | 25 | set color (value: THREE.Color) { 26 | this.light.color = value 27 | } 28 | 29 | /** 30 | * The intensity of the light. 31 | */ 32 | get intensity () { 33 | return this.light.intensity 34 | } 35 | 36 | set intensity (value: number) { 37 | this.light.intensity = value 38 | } 39 | 40 | /** 41 | * Whether the light follows the camera or not. 42 | */ 43 | get followCamera () { 44 | return this._unsubscribe !== undefined 45 | } 46 | 47 | set followCamera (value: boolean) { 48 | if (this.followCamera === value) return 49 | 50 | this._unsubscribe?.() 51 | this._unsubscribe = undefined 52 | 53 | if (value) { 54 | this._unsubscribe = this._camera.onMoved.subscribe(() => this.updateLightPosition()) 55 | this.updateLightPosition() 56 | } 57 | } 58 | 59 | constructor ( 60 | camera: ICamera, 61 | options: { 62 | followCamera: boolean, 63 | position: THREE.Vector3, 64 | color: THREE.Color, 65 | intensity: number 66 | } 67 | ) { 68 | this._camera = camera 69 | this.position = options.position.clone() 70 | this.light = new THREE.DirectionalLight(options.color, options.intensity) 71 | this.followCamera = options.followCamera 72 | } 73 | 74 | /** 75 | * Updates the light's position based on the camera's quaternion. 76 | */ 77 | private updateLightPosition () { 78 | this.light.position.copy(this.position).applyQuaternion(this._camera.quaternion) 79 | } 80 | 81 | /** 82 | * Disposes of the camera light. 83 | */ 84 | dispose () { 85 | this._unsubscribe?.() 86 | this._unsubscribe = undefined 87 | this.light.dispose() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/environment/environment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { ViewerSettings } from '../settings/viewerSettings' 7 | import { ICamera } from '../camera/camera' 8 | import { ViewerMaterials } from '../../vim-loader/materials/viewerMaterials' 9 | import { GroundPlane } from './groundPlane' 10 | import { Skybox } from './skybox' 11 | import { Renderer } from '../rendering/renderer' 12 | import { CameraLight } from './cameraLight' 13 | /** 14 | * Manages ground plane and lights that are part of the THREE.Scene to render but not part of the Vims. 15 | */ 16 | export class Environment { 17 | private readonly _renderer: Renderer 18 | private readonly _camera: ICamera 19 | 20 | /** 21 | * The skylight in the scene. 22 | */ 23 | readonly skyLight: THREE.HemisphereLight 24 | 25 | /** 26 | * The array of directional lights in the scene. 27 | */ 28 | readonly sunLights: ReadonlyArray 29 | 30 | /** 31 | * The ground plane under the model in the scene. 32 | */ 33 | readonly groundPlane: GroundPlane 34 | 35 | /* 36 | * The skybox in the scene. 37 | */ 38 | readonly skybox: Skybox 39 | 40 | constructor (camera:ICamera, renderer: Renderer, viewerMaterials: ViewerMaterials, settings: ViewerSettings) { 41 | this._camera = camera 42 | this._renderer = renderer 43 | 44 | this.groundPlane = new GroundPlane(settings) 45 | this.skyLight = this.createSkyLight(settings) 46 | this.skybox = new Skybox(camera, renderer, viewerMaterials, settings) 47 | this.sunLights = this.createSunLights(settings) 48 | 49 | this.setupRendererListeners() 50 | this.addObjectsToRenderer() 51 | } 52 | 53 | /** 54 | * Returns all three objects composing the environment 55 | */ 56 | private getObjects (): ReadonlyArray { 57 | return [this.groundPlane.mesh, this.skyLight, ...this.sunLights.map(l => l.light), this.skybox.mesh] 58 | } 59 | 60 | private createSkyLight (settings: ViewerSettings): THREE.HemisphereLight { 61 | const { skyColor, groundColor, intensity } = settings.skylight 62 | return new THREE.HemisphereLight(skyColor, groundColor, intensity) 63 | } 64 | 65 | private createSunLights (settings: ViewerSettings): ReadonlyArray { 66 | return settings.sunlights.map((s) => 67 | new CameraLight(this._camera, s) 68 | ) 69 | } 70 | 71 | private addObjectsToRenderer (): void { 72 | this.getObjects().forEach((o) => this._renderer.add(o)) 73 | } 74 | 75 | private setupRendererListeners (): void { 76 | this._renderer.onBoxUpdated.subscribe(() => { 77 | const box = this._renderer.getBoundingBox() 78 | this.groundPlane.adaptToContent(box) 79 | }) 80 | } 81 | 82 | /** 83 | * Dispose of all resources. 84 | */ 85 | dispose (): void { 86 | this.getObjects().forEach((o) => this._renderer.remove(o)) 87 | this.sunLights.forEach((s) => s.dispose()) 88 | this.skyLight.dispose() 89 | this.groundPlane.dispose() 90 | this.skybox.dispose() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/environment/groundPlane.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { TextureEncoding, ViewerSettings } from '../settings/viewerSettings' 7 | 8 | /** 9 | * Manages the THREE.Mesh for the ground plane under the vims 10 | */ 11 | export class GroundPlane { 12 | readonly mesh: THREE.Mesh 13 | 14 | private _source: string | undefined 15 | private _size: number = 1 16 | 17 | /** 18 | * Whether the ground plane is visible or not. 19 | */ 20 | get visible () { 21 | return this.mesh.visible 22 | } 23 | 24 | set visible (value: boolean) { 25 | this.mesh.visible = value 26 | } 27 | 28 | // disposable 29 | private _geometry: THREE.PlaneGeometry 30 | private _material: THREE.MeshBasicMaterial 31 | private _texture: THREE.Texture | undefined 32 | 33 | constructor (settings : ViewerSettings) { 34 | this._geometry = new THREE.PlaneGeometry() 35 | this._material = new THREE.MeshBasicMaterial({ 36 | transparent: true, 37 | depthTest: true, 38 | depthWrite: false 39 | }) 40 | this.mesh = new THREE.Mesh(this._geometry, this._material) 41 | // Makes ground plane be drawn first so that isolation material is drawn on top. 42 | this.mesh.renderOrder = -1 43 | 44 | this._size = settings.groundPlane.size 45 | // Visibily 46 | this.mesh.visible = settings.groundPlane.visible 47 | 48 | // Looks 49 | this.applyTexture( 50 | settings.groundPlane.encoding, 51 | settings.groundPlane.texture 52 | ) 53 | this._material.color.copy(settings.groundPlane.color) 54 | this._material.opacity = settings.groundPlane.opacity 55 | } 56 | 57 | adaptToContent (box: THREE.Box3) { 58 | // Position 59 | const center = box.getCenter(new THREE.Vector3()) 60 | const position = new THREE.Vector3( 61 | center.x, 62 | box.min.y - Math.abs(box.min.y) * 0.01, 63 | center.z 64 | ) 65 | this.mesh.position.copy(position) 66 | // Rotation 67 | // Face up, rotate by 270 degrees around x 68 | this.mesh.quaternion.copy( 69 | new THREE.Quaternion().setFromEuler(new THREE.Euler(1.5 * Math.PI, 0, 0)) 70 | ) 71 | 72 | // Scale 73 | const sphere = box?.getBoundingSphere(new THREE.Sphere()) 74 | const size = (sphere?.radius ?? 1) * this._size 75 | const scale = new THREE.Vector3(1, 1, 1).multiplyScalar(size) 76 | this.mesh.scale.copy(scale) 77 | } 78 | 79 | applyTexture (encoding: TextureEncoding, source: string) { 80 | // Check for changes 81 | if (source === this._source) return 82 | this._source = source 83 | 84 | // dispose previous texture 85 | this._texture?.dispose() 86 | this._texture = undefined 87 | // Bail if new texture url, is no texture 88 | if (!source || !encoding) return 89 | 90 | if (encoding === 'url') { 91 | // load texture 92 | const loader = new THREE.TextureLoader() 93 | this._texture = loader.load(source) 94 | } 95 | if (encoding === 'base64') { 96 | const image = new Image() 97 | image.src = source 98 | const txt = new THREE.Texture() 99 | this._texture = txt 100 | this._texture.image = image 101 | image.onload = () => { 102 | txt.needsUpdate = true 103 | } 104 | } 105 | if (!this._texture) { 106 | console.error('Failed to load texture: ' + source) 107 | return 108 | } 109 | 110 | // Apply texture 111 | this._material.map = this._texture 112 | } 113 | 114 | dispose () { 115 | this._geometry?.dispose() 116 | this._material?.dispose() 117 | this._texture?.dispose() 118 | 119 | this._texture = undefined 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/environment/skybox.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { ViewerSettings } from '../settings/viewerSettings' 7 | import { ICamera } from '../camera/camera' 8 | import { ViewerMaterials } from '../../vim-loader/materials/viewerMaterials' 9 | import { SkyboxMaterial } from '../../vim-loader/materials/skyboxMaterial' 10 | import { Renderer } from '../rendering/renderer' 11 | 12 | export class Skybox { 13 | readonly mesh : THREE.Mesh 14 | 15 | /** 16 | * Whether the skybox is enabled or not. 17 | */ 18 | get enable () { 19 | return this.mesh.visible 20 | } 21 | 22 | /** 23 | * Whether the skybox is enabled or not. 24 | */ 25 | set enable (value: boolean) { 26 | this.mesh.visible = value 27 | this._renderer.needsUpdate = true 28 | } 29 | 30 | /** 31 | * The color of the sky. 32 | */ 33 | get skyColor () { 34 | return this._material.skyColor 35 | } 36 | 37 | set skyColor (value: THREE.Color) { 38 | this._material.skyColor = value 39 | this._renderer.needsUpdate = true 40 | } 41 | 42 | /** 43 | * The color of the ground. 44 | */ 45 | get groundColor () { 46 | return this._material.groundColor 47 | } 48 | 49 | set groundColor (value: THREE.Color) { 50 | this._material.groundColor = value 51 | this._renderer.needsUpdate = true 52 | } 53 | 54 | /** 55 | * The sharpness of the gradient transition between the sky and the ground. 56 | */ 57 | get sharpness () { 58 | return this._material.sharpness 59 | } 60 | 61 | set sharpness (value: number) { 62 | this._material.sharpness = value 63 | this._renderer.needsUpdate = true 64 | } 65 | 66 | private readonly _plane : THREE.PlaneGeometry 67 | private readonly _material : SkyboxMaterial 68 | private readonly _renderer: Renderer 69 | 70 | constructor (camera: ICamera, renderer : Renderer, materials: ViewerMaterials, settings: ViewerSettings) { 71 | this._renderer = renderer 72 | this._plane = new THREE.PlaneGeometry() 73 | this._material = materials.skyBox 74 | this.mesh = new THREE.Mesh(this._plane, materials.skyBox) 75 | 76 | // Apply settings 77 | this.enable = settings.skybox.enable 78 | this.skyColor = settings.skybox.skyColor 79 | this.groundColor = settings.skybox.groundColor 80 | this.sharpness = settings.skybox.sharpness 81 | 82 | camera.onMoved.subscribe(() => { 83 | this.mesh.position.copy(camera.position).add(camera.forward) 84 | this.mesh.quaternion.copy(camera.quaternion) 85 | const size = camera.frustrumSizeAt(this.mesh.position) 86 | this.mesh.scale.set(size.x, size.y, 1) 87 | }) 88 | } 89 | 90 | dispose () { 91 | this._plane.dispose() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/axes/axes.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { AxesSettings } from './axesSettings' 3 | 4 | export class Axis { 5 | axis: string 6 | direction: THREE.Vector3 7 | size: number 8 | color: string 9 | colorSub: string 10 | position: THREE.Vector3 11 | 12 | // Optional 13 | label: string | undefined 14 | line: number | undefined 15 | 16 | constructor (init: Axis) { 17 | this.axis = init.axis 18 | this.direction = init.direction 19 | this.size = init.size 20 | this.position = init.position 21 | this.color = init.color 22 | this.colorSub = init.colorSub 23 | 24 | // Optional 25 | this.line = init.line 26 | this.label = init.label 27 | } 28 | } 29 | 30 | export function createAxes (settings : AxesSettings) { 31 | return [ 32 | new Axis({ 33 | axis: 'x', 34 | direction: new THREE.Vector3(1, 0, 0), 35 | size: settings.bubbleSizePrimary, 36 | color: settings.colorX, 37 | colorSub: settings.colorXSub, 38 | line: settings.lineWidth, 39 | label: 'X', 40 | position: new THREE.Vector3(0, 0, 0) 41 | }), 42 | new Axis({ 43 | axis: 'y', 44 | direction: new THREE.Vector3(0, 1, 0), 45 | size: settings.bubbleSizePrimary, 46 | color: settings.colorY, 47 | colorSub: settings.colorYSub, 48 | line: settings.lineWidth, 49 | label: 'Y', 50 | position: new THREE.Vector3(0, 0, 0) 51 | }), 52 | new Axis({ 53 | axis: 'z', 54 | direction: new THREE.Vector3(0, 0, 1), 55 | size: settings.bubbleSizePrimary, 56 | color: settings.colorZ, 57 | colorSub: settings.colorZSub, 58 | line: settings.lineWidth, 59 | label: 'Z', 60 | position: new THREE.Vector3(0, 0, 0) 61 | }), 62 | new Axis({ 63 | axis: '-x', 64 | direction: new THREE.Vector3(-1, 0, 0), 65 | size: settings.bubbleSizeSecondary, 66 | color: settings.colorX, 67 | colorSub: settings.colorXSub, 68 | line: undefined, 69 | label: undefined, 70 | position: new THREE.Vector3(0, 0, 0) 71 | }), 72 | new Axis({ 73 | axis: '-y', 74 | direction: new THREE.Vector3(0, -1, 0), 75 | size: settings.bubbleSizeSecondary, 76 | color: settings.colorY, 77 | colorSub: settings.colorYSub, 78 | line: undefined, 79 | label: undefined, 80 | position: new THREE.Vector3(0, 0, 0) 81 | }), 82 | new Axis({ 83 | axis: '-z', 84 | // inverted Z 85 | direction: new THREE.Vector3(0, 0, -1), 86 | size: settings.bubbleSizeSecondary, 87 | color: settings.colorZ, 88 | colorSub: settings.colorZSub, 89 | line: undefined, 90 | label: undefined, 91 | position: new THREE.Vector3(0, 0, 0) 92 | }) 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/axes/axesSettings.ts: -------------------------------------------------------------------------------- 1 | export class AxesSettings { 2 | size: number = 84 3 | padding: number = 4 4 | bubbleSizePrimary: number = 8 5 | bubbleSizeSecondary: number = 6 6 | lineWidth: number = 2 7 | fontPxSize: number = 12 8 | fontFamily: string = 'arial' 9 | fontWeight: string = 'bold' 10 | fontColor: string = '#222222' 11 | className: string = 'gizmo-axis-canvas' 12 | 13 | colorX: string = '#f73c3c' 14 | colorY: string = '#6ccb26' 15 | colorZ: string = '#178cf0' 16 | colorXSub: string = '#942424' 17 | colorYSub: string = '#417a17' 18 | colorZSub: string = '#0e5490' 19 | 20 | constructor (init?: Partial) { 21 | this.size = init?.size ?? this.size 22 | this.padding = init?.padding ?? this.padding 23 | this.bubbleSizePrimary = init?.bubbleSizePrimary ?? this.bubbleSizePrimary 24 | this.bubbleSizeSecondary = 25 | init?.bubbleSizeSecondary ?? this.bubbleSizeSecondary 26 | this.lineWidth = init?.lineWidth ?? this.lineWidth 27 | this.fontPxSize = init?.fontPxSize ?? this.fontPxSize 28 | this.fontFamily = init?.fontFamily ?? this.fontFamily 29 | this.fontWeight = init?.fontWeight ?? this.fontWeight 30 | this.fontColor = init?.fontColor ?? this.fontColor 31 | this.className = init?.className ?? this.className 32 | this.colorX = init?.colorX ?? this.colorX 33 | this.colorY = init?.colorY ?? this.colorY 34 | this.colorZ = init?.colorZ ?? this.colorZ 35 | this.colorXSub = init?.colorXSub ?? this.colorXSub 36 | this.colorYSub = init?.colorYSub ?? this.colorYSub 37 | this.colorZSub = init?.colorZSub ?? this.colorZSub 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/gizmoLoading.ts: -------------------------------------------------------------------------------- 1 | /** 2 | @module viw-webgl-viewer/gizmos/sectionBox 3 | */ 4 | 5 | import { Viewer } from '../viewer' 6 | 7 | /** 8 | * The loading indicator gizmo. 9 | */ 10 | export class GizmoLoading { 11 | // dependencies 12 | private _viewer: Viewer 13 | private _spinner: HTMLElement 14 | private _visible: boolean 15 | 16 | constructor (viewer: Viewer) { 17 | this._viewer = viewer 18 | this._spinner = this.createBar() 19 | this._visible = false 20 | } 21 | 22 | private createBar () { 23 | const div = document.createElement('span') 24 | div.className = 'loader' 25 | return div 26 | } 27 | 28 | /** 29 | * Indicates whether the loading gizmo will be rendered. 30 | */ 31 | get visible () { 32 | return this._visible 33 | } 34 | 35 | set visible (value: boolean) { 36 | if (!this._visible && value) { 37 | this._viewer.viewport.canvas.parentElement.appendChild(this._spinner) 38 | this._visible = true 39 | } 40 | if (this._visible && !value) { 41 | this._spinner.parentElement.removeChild(this._spinner) 42 | this._visible = false 43 | } 44 | } 45 | 46 | /** 47 | * Disposes of all resources. 48 | */ 49 | dispose () { 50 | this.visible = false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/gizmoRectangle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/gizmos 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { Viewer } from '../viewer' 7 | 8 | /** 9 | * Rectangle Gizmo used for rectangle selection. 10 | */ 11 | export class GizmoRectangle { 12 | private line: THREE.LineSegments 13 | private viewer: Viewer 14 | private points: THREE.Vector3[] | undefined 15 | 16 | constructor (viewer: Viewer) { 17 | this.viewer = viewer 18 | 19 | const mat = new THREE.LineBasicMaterial({ 20 | depthTest: false, 21 | color: new THREE.Color(0x00ff00), 22 | // Transparent so it is drawn in the transparent pass, and always appear on top. 23 | transparent: true, 24 | opacity: 1 25 | }) 26 | 27 | // prettier-ignore 28 | const vertices = new Float32Array([ 29 | -0.5, -0.5, 0, 30 | 0.5, -0.5, 0, 31 | 32 | 0.5, -0.5, 0, 33 | 0.5, 0.5, 0, 34 | 35 | 0.5, 0.5, 0, 36 | -0.5, 0.5, 0, 37 | 38 | -0.5, 0.5, 0, 39 | -0.5, -0.5, 0 40 | ]) 41 | 42 | const geo = new THREE.BufferGeometry() 43 | geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3)) 44 | 45 | this.line = new THREE.LineSegments(geo, mat) 46 | this.line.renderOrder = 1 47 | this.line.name = 'GizmoSelection' 48 | this.line.visible = false 49 | this.viewer.renderer.add(this.line) 50 | } 51 | 52 | /** 53 | * Removes the object from rendering and dispose resources from memory 54 | */ 55 | dispose () { 56 | this.viewer.renderer.remove(this.line) 57 | this.line.geometry.dispose() 58 | ;(this.line.material as THREE.Material).dispose() 59 | } 60 | 61 | /** 62 | * Indicates whether the gizmo is visible. 63 | */ 64 | get visible () { 65 | return this.line.visible 66 | } 67 | 68 | set visible (value: boolean) { 69 | if (value === this.line.visible) return 70 | this.viewer.renderer.needsUpdate = true 71 | this.line.visible = value 72 | } 73 | 74 | /** 75 | * Sets the two corner points defining the rectangle. 76 | * @param {THREE.Vector2} posA - The position of the first corner. 77 | * @param {THREE.Vector2} posB - The position of the second corner. 78 | */ 79 | setCorners (posA: THREE.Vector2, posB: THREE.Vector2) { 80 | // Plane perpedicular to camera 81 | const plane = new THREE.Plane().setFromNormalAndCoplanarPoint( 82 | this.viewer.camera.forward, 83 | this.viewer.camera.target 84 | ) 85 | 86 | // Points intersections with plane 87 | const A = this.getIntersection(plane, posA) 88 | const B = this.getIntersection(plane, posB) 89 | if (!A || !B) return 90 | 91 | // Center is average of both points. 92 | const center = A.clone().add(B).multiplyScalar(0.5) 93 | 94 | const [dx, dy] = this.getBoxSize(A, B) 95 | this.updateRect(center, dx, dy) 96 | 97 | // Keep 4 corners and center for bounding box 98 | const AB = this.getIntersection(plane, new THREE.Vector2(posA.x, posB.y)) 99 | const BA = this.getIntersection(plane, new THREE.Vector2(posB.x, posA.y)) 100 | if (!AB || !BA) return 101 | 102 | this.points = [A, B, AB, BA, center] 103 | } 104 | 105 | private getIntersection (plane: THREE.Plane, position: THREE.Vector2) { 106 | const raycaster = this.viewer.raycaster.fromPoint2(position) 107 | return raycaster.ray.intersectPlane(plane, new THREE.Vector3()) ?? undefined 108 | } 109 | 110 | private updateRect (position: THREE.Vector3, dx: number, dy: number) { 111 | // Update rectangle transform 112 | this.line.quaternion.copy(this.viewer.camera.quaternion) 113 | this.line.position.copy(position) 114 | this.line.scale.set(dx, dy, 1) 115 | this.line.updateMatrix() 116 | this.viewer.renderer.needsUpdate = true 117 | } 118 | 119 | private getBoxSize (A: THREE.Vector3, B: THREE.Vector3) { 120 | const cam = this.viewer.camera 121 | // Compute the basis components of the projection plane. 122 | const up = new THREE.Vector3(0, 1, 0).applyQuaternion(cam.quaternion) 123 | const right = new THREE.Vector3(1, 0, 0).applyQuaternion(cam.quaternion) 124 | 125 | // Transform the 3d positions to 2d on the projection plane. 126 | const Ax = A.dot(right) 127 | const Ay = A.dot(up) 128 | const Bx = B.dot(right) 129 | const By = B.dot(up) 130 | 131 | // Compute rectangle size 132 | const dx = Math.abs(Ax - Bx) 133 | const dy = Math.abs(Ay - By) 134 | return [dx, dy] 135 | } 136 | 137 | /** 138 | * Returns the bounding box of the selection. 139 | * The bounding box is the projection of the selection rectangle 140 | * onto the plane coplanar to the closest hit of 5 raycasts: one in each corner and one in the center. 141 | * X-----X 142 | * | X | 143 | * X-----X 144 | * @returns {THREE.Box3} The bounding box of the selection. 145 | */ 146 | getBoundingBox (target: THREE.Box3 = new THREE.Box3()) { 147 | const position = this.getClosestHit() 148 | const projections = position ? this.projectPoints(position) : this.points 149 | if (!projections) return 150 | return target.setFromPoints(projections) 151 | } 152 | 153 | /** 154 | * Performs a raycast from the camera through the five interest points and returns the closest hit position. 155 | * @returns {THREE.Vector3} The position of the closest hit. 156 | */ 157 | getClosestHit () { 158 | if (!this.points) return 159 | 160 | const hits = this.points 161 | .map((p) => this.viewer.raycaster.raycast3(p)) 162 | .filter((h) => h.isHit) 163 | 164 | let position: THREE.Vector3 | undefined 165 | let dist: number 166 | hits.forEach((h) => { 167 | if ( 168 | h.distance !== undefined && 169 | h.position !== undefined && 170 | (dist === undefined || h.distance < dist) 171 | ) { 172 | dist = h.distance 173 | position = h.position 174 | } 175 | }) 176 | return position 177 | } 178 | 179 | /** 180 | * Projects all points on a plane the coplanar to position. 181 | */ 182 | private projectPoints (position: THREE.Vector3) { 183 | const plane = new THREE.Plane().setFromNormalAndCoplanarPoint( 184 | this.viewer.camera.forward, 185 | position 186 | ) 187 | 188 | return this.points?.map((p) => plane.projectPoint(p, new THREE.Vector3())) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/gizmos.ts: -------------------------------------------------------------------------------- 1 | import { Viewer } from '../viewer' 2 | import { GizmoAxes } from './axes/gizmoAxes' 3 | import { GizmoLoading } from './gizmoLoading' 4 | import { GizmoOrbit } from './gizmoOrbit' 5 | import { GizmoRectangle } from './gizmoRectangle' 6 | import { IMeasure, Measure } from './measure/measure' 7 | import { SectionBox } from './sectionBox/sectionBox' 8 | import { GizmoMarkers } from './markers/gizmoMarkers' 9 | import { Camera } from '../camera/camera' 10 | 11 | /** 12 | * Represents a collection of gizmos used for various visualization and interaction purposes within the viewer. 13 | */ 14 | export class Gizmos { 15 | private readonly viewer: Viewer 16 | 17 | /** 18 | * The interface to start and manage measure tool interaction. 19 | */ 20 | get measure () { 21 | return this._measure as IMeasure 22 | } 23 | 24 | private readonly _measure: Measure 25 | 26 | /** 27 | * The section box gizmo. 28 | */ 29 | readonly section: SectionBox 30 | 31 | /** 32 | * The loading indicator gizmo. 33 | */ 34 | readonly loading: GizmoLoading 35 | 36 | /** 37 | * The camera orbit target gizmo. 38 | */ 39 | readonly orbit: GizmoOrbit 40 | 41 | /** 42 | * Rectangle Gizmo used for rectangle selection. 43 | */ 44 | readonly rectangle: GizmoRectangle 45 | 46 | /** 47 | * The axis gizmos of the viewer. 48 | */ 49 | readonly axes: GizmoAxes 50 | 51 | /** 52 | * The interface for adding and managing sprite markers in the scene. 53 | */ 54 | readonly markers: GizmoMarkers 55 | 56 | constructor (viewer: Viewer, camera : Camera) { 57 | this.viewer = viewer 58 | this._measure = new Measure(viewer) 59 | this.section = new SectionBox(viewer) 60 | this.loading = new GizmoLoading(viewer) 61 | this.orbit = new GizmoOrbit( 62 | viewer.renderer, 63 | camera, 64 | viewer.inputs, 65 | viewer.settings 66 | ) 67 | this.rectangle = new GizmoRectangle(viewer) 68 | this.axes = new GizmoAxes(camera, viewer.viewport, viewer.settings.axes) 69 | this.markers = new GizmoMarkers(viewer) 70 | viewer.viewport.canvas.parentElement?.prepend(this.axes.canvas) 71 | } 72 | 73 | updateAfterCamera () { 74 | this.axes.update() 75 | } 76 | 77 | /** 78 | * Disposes of all gizmos. 79 | */ 80 | dispose () { 81 | this.viewer.viewport.canvas.parentElement?.removeChild(this.axes.canvas) 82 | this._measure.clear() 83 | this.section.dispose() 84 | this.loading.dispose() 85 | this.orbit.dispose() 86 | this.rectangle.dispose() 87 | this.axes.dispose() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/markers/gizmoMarker.ts: -------------------------------------------------------------------------------- 1 | import { Vim } from '../../../vim-loader/vim' 2 | import { Viewer } from '../../viewer' 3 | import * as THREE from 'three' 4 | import { SimpleInstanceSubmesh } from '../../../vim-loader/mesh' 5 | import { ObjectAttribute } from '../../../vim-loader/objectAttributes' 6 | import { ColorAttribute } from '../../../vim-loader/colorAttributes' 7 | 8 | /** 9 | * Marker gizmo that display an interactive sphere at a 3D positions 10 | * Marker gizmos are still under development. 11 | */ 12 | export class GizmoMarker { 13 | public readonly type = 'Marker' 14 | private _viewer: Viewer 15 | private _submesh: SimpleInstanceSubmesh 16 | 17 | /** 18 | * The vim object from which this object came from. 19 | */ 20 | vim: Vim | undefined 21 | 22 | /** 23 | * The bim element index associated with this object. 24 | */ 25 | element: number | undefined 26 | 27 | /** 28 | * The geometry instances associated with this object. 29 | */ 30 | instances: number[] | undefined 31 | 32 | private _outlineAttribute: ObjectAttribute 33 | private _visibleAttribute: ObjectAttribute 34 | private _coloredAttribute: ObjectAttribute 35 | private _focusedAttribute: ObjectAttribute 36 | private _colorAttribute: ColorAttribute 37 | 38 | constructor (viewer: Viewer, submesh: SimpleInstanceSubmesh) { 39 | this._viewer = viewer 40 | this._submesh = submesh 41 | 42 | const array = [submesh] 43 | this._outlineAttribute = new ObjectAttribute( 44 | false, 45 | 'selected', 46 | 'selected', 47 | array, 48 | (v) => (v ? 1 : 0) 49 | ) 50 | 51 | this._visibleAttribute = new ObjectAttribute( 52 | true, 53 | 'ignore', 54 | 'ignore', 55 | array, 56 | (v) => (v ? 0 : 1) 57 | ) 58 | 59 | this._focusedAttribute = new ObjectAttribute( 60 | false, 61 | 'focused', 62 | 'focused', 63 | array, 64 | (v) => (v ? 1 : 0) 65 | ) 66 | 67 | this._coloredAttribute = new ObjectAttribute( 68 | false, 69 | 'colored', 70 | 'colored', 71 | array, 72 | (v) => (v ? 1 : 0) 73 | ) 74 | 75 | this._colorAttribute = new ColorAttribute(array, undefined, undefined) 76 | this.color = new THREE.Color(0xff1a1a) 77 | } 78 | 79 | updateMesh (mesh: SimpleInstanceSubmesh) { 80 | this._submesh = mesh 81 | const array = [this._submesh] 82 | this._visibleAttribute.updateMeshes(array) 83 | this._focusedAttribute.updateMeshes(array) 84 | this._outlineAttribute.updateMeshes(array) 85 | this._colorAttribute.updateMeshes(array) 86 | this._coloredAttribute.updateMeshes(array) 87 | this._viewer.renderer.needsUpdate = true 88 | } 89 | 90 | /** Sets the position of the marker in the 3d scene */ 91 | set position (value: THREE.Vector3) { 92 | const m = new THREE.Matrix4() 93 | m.compose(value, new THREE.Quaternion(), new THREE.Vector3(1, 1, 1)) 94 | this._submesh.mesh.setMatrixAt(this._submesh.index, m) 95 | this._submesh.mesh.instanceMatrix.needsUpdate = true 96 | } 97 | 98 | get position () { 99 | const m = new THREE.Matrix4() 100 | this._submesh.mesh.getMatrixAt(0, m) 101 | return new THREE.Vector3().setFromMatrixPosition(m) 102 | } 103 | 104 | /** 105 | * Always false 106 | */ 107 | get hasMesh (): boolean { 108 | return false 109 | } 110 | 111 | /** 112 | * Applies a color override instead of outlines. 113 | */ 114 | get outline (): boolean { 115 | return this._outlineAttribute.value 116 | } 117 | 118 | set outline (value: boolean) { 119 | this._outlineAttribute.apply(value) 120 | } 121 | 122 | /** 123 | * Enlarges the gizmo to indicate focus. 124 | */ 125 | get focused (): boolean { 126 | return this._focusedAttribute.value 127 | } 128 | 129 | set focused (value: boolean) { 130 | this._focusedAttribute.apply(value) 131 | this._viewer.renderer.needsUpdate = true 132 | } 133 | 134 | /** 135 | * Determines if the gizmo will be rendered. 136 | */ 137 | get visible (): boolean { 138 | return this._visibleAttribute.value 139 | } 140 | 141 | set visible (value: boolean) { 142 | this._visibleAttribute.apply(value) 143 | this._viewer.renderer.needsUpdate = true 144 | } 145 | 146 | get color (): THREE.Color { 147 | return this._colorAttribute.value 148 | } 149 | 150 | set color (color: THREE.Color) { 151 | if (color) { 152 | this._coloredAttribute.apply(true) 153 | this._colorAttribute.apply(color) 154 | } else { 155 | this._coloredAttribute.apply(false) 156 | } 157 | this._viewer.renderer.needsUpdate = true 158 | } 159 | 160 | get size () { 161 | const matrix = new THREE.Matrix4() 162 | this._submesh.mesh.getMatrixAt(this._submesh.index, matrix) 163 | return matrix.elements[0] 164 | } 165 | 166 | set size (value: number) { 167 | const matrix = new THREE.Matrix4() 168 | this._submesh.mesh.getMatrixAt(this._submesh.index, matrix) 169 | matrix.elements[0] = value 170 | matrix.elements[5] = value 171 | matrix.elements[10] = value 172 | this._submesh.mesh.setMatrixAt(this._submesh.index, matrix) 173 | this._submesh.mesh.instanceMatrix.needsUpdate = true 174 | this._viewer.renderer.needsUpdate = true 175 | } 176 | 177 | /** 178 | * Retrieves the bounding box of the object from cache or computes it if needed. 179 | * Returns a unit box arount the marker position. 180 | * @returns {THREE.Box3 | undefined} The bounding box of the object. 181 | */ 182 | getBoundingBox (): THREE.Box3 { 183 | return new THREE.Box3().setFromCenterAndSize(this.position.clone(), new THREE.Vector3(1, 1, 1)) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/markers/gizmoMarkers.ts: -------------------------------------------------------------------------------- 1 | import { Viewer } from '../../viewer' 2 | import * as THREE from 'three' 3 | import { GizmoMarker } from './gizmoMarker' 4 | import { StandardMaterial } from '../../../vim-loader/materials/standardMaterial' 5 | import { SimpleInstanceSubmesh } from '../../../vim-loader/mesh' 6 | 7 | /** 8 | * API for adding and managing sprite markers in the scene. 9 | */ 10 | export class GizmoMarkers { 11 | private _viewer: Viewer 12 | private _markers: GizmoMarker[] = [] 13 | private _mesh : THREE.InstancedMesh 14 | 15 | constructor (viewer: Viewer) { 16 | this._viewer = viewer 17 | this._mesh = this.createMesh(undefined, 1, 0) 18 | 19 | this._mesh.count = 0 20 | } 21 | 22 | getMarkerFromIndex (index: number) { 23 | return this._markers[index] 24 | } 25 | 26 | private createMesh (previous : THREE.InstancedMesh, capacity : number, count: number) { 27 | const geometry = previous?.geometry ?? new THREE.SphereBufferGeometry(1, 8, 8) 28 | 29 | const mat = previous?.material ?? new StandardMaterial(new THREE.MeshPhongMaterial({ 30 | color: 0x999999, 31 | vertexColors: true, 32 | flatShading: true, 33 | shininess: 1, 34 | transparent: true, 35 | depthTest: false 36 | })).material 37 | 38 | const mesh = new THREE.InstancedMesh(geometry, mat, capacity) 39 | mesh.renderOrder = 100 40 | mesh.userData.vim = this 41 | mesh.count = count 42 | 43 | this._viewer.renderer.add(mesh) 44 | return mesh 45 | } 46 | 47 | private resizeMesh () { 48 | const larger = this.createMesh(this._mesh, this._mesh.count * 2, this._mesh.count) 49 | 50 | for (let i = 0; i < this._mesh.count; i++) { 51 | const m = new THREE.Matrix4() 52 | this._mesh.getMatrixAt(i, m) 53 | larger.setMatrixAt(i, m) 54 | const sub = new SimpleInstanceSubmesh(larger, i) 55 | this._markers[i].updateMesh(sub) 56 | } 57 | 58 | this._viewer.renderer.remove(this._mesh) 59 | this._mesh = larger 60 | } 61 | 62 | /** 63 | * Adds a sprite marker at the specified position. 64 | * @param {THREE.Vector3} position - The position at which to add the marker. 65 | */ 66 | add (position: THREE.Vector3) { 67 | if (this._mesh.count === this._mesh.instanceMatrix.count) { 68 | this.resizeMesh() 69 | } 70 | 71 | this._mesh.count += 1 72 | const sub = new SimpleInstanceSubmesh(this._mesh, this._mesh.count - 1) 73 | const marker = new GizmoMarker(this._viewer, sub) 74 | marker.position = position 75 | this._markers.push(marker) 76 | 77 | return marker 78 | } 79 | 80 | /** 81 | * Removes the specified marker from the scene. 82 | * @param {GizmoMarker} marker - The marker to remove. 83 | */ 84 | remove (marker: GizmoMarker) { 85 | const index = this._markers.findIndex(m => m === marker) 86 | if (index < 0) return 87 | 88 | this._markers[index] = this._markers[this._markers.length - 1] 89 | this._markers.length -= 1 90 | this._mesh.count -= 1 91 | 92 | // No replacement when removing the last marker 93 | const replacement = this._markers[index] 94 | if (replacement) { 95 | const sub = new SimpleInstanceSubmesh(this._mesh, index) 96 | replacement.updateMesh(sub) 97 | } 98 | 99 | this._viewer.renderer.needsUpdate = true 100 | } 101 | 102 | /** 103 | * Removes all markers from the scene. 104 | */ 105 | clear () { 106 | this._mesh.count = 0 107 | this._markers.length = 0 108 | this._viewer.renderer.needsUpdate = true 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/measure/measure.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/gizmos/measure 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { InputScheme } from '../../inputs/input' 7 | import { InputAction } from '../../raycaster' 8 | import { Viewer } from '../../viewer' 9 | import { MeasureFlow, MeasureStage } from './measureFlow' 10 | import { MeasureGizmo } from './measureGizmo' 11 | 12 | /** 13 | * Interacts with the measure tool. 14 | */ 15 | export interface IMeasure { 16 | /** 17 | * Start point of the current measure or undefined if no active measure. 18 | */ 19 | get startPoint(): THREE.Vector3 | undefined 20 | 21 | /** 22 | * End point of the current measure or undefined if no active measure. 23 | */ 24 | get endPoint(): THREE.Vector3 | undefined 25 | 26 | /** 27 | * Vector from start to end of the current measure or undefined if no active measure. 28 | */ 29 | get measurement(): THREE.Vector3 | undefined 30 | 31 | /** 32 | * Stage of the current measure or undefined if no active measure. 33 | */ 34 | get stage(): MeasureStage | undefined 35 | 36 | /** 37 | * Starts a new measure flow where the two next click are overriden. 38 | * Currently running flow if any will be aborted. 39 | * Promise is resolved if flow is succesfully completed, rejected otherwise. 40 | * Do not override viewer.onMouseClick while this flow is active. 41 | */ 42 | start(onProgress?: () => void): Promise 43 | 44 | /** 45 | * Aborts the current measure flow, fails the related promise. 46 | */ 47 | abort(): void 48 | 49 | /** 50 | * Clears meshes. 51 | */ 52 | clear(): void 53 | } 54 | 55 | /** 56 | * Manages measure flow and gizmos 57 | */ 58 | export class Measure implements IMeasure { 59 | // dependencies 60 | private _viewer: Viewer 61 | 62 | // resources 63 | private _meshes: MeasureGizmo | undefined 64 | 65 | // results 66 | private _startPos: THREE.Vector3 | undefined 67 | 68 | private _endPos: THREE.Vector3 | undefined 69 | private _measurement: THREE.Vector3 | undefined 70 | private _flow: MeasureFlow | undefined 71 | private _previousScheme: InputScheme 72 | 73 | /** 74 | * Start point of the current measure or undefined if no active measure. 75 | */ 76 | get startPoint () { 77 | return this._startPos 78 | } 79 | 80 | /** 81 | * End point of the current measure or undefined if no active measure. 82 | */ 83 | get endPoint () { 84 | return this._endPos 85 | } 86 | 87 | /** 88 | * Vector from start to end of the current measure or undefined if no active measure. 89 | */ 90 | get measurement () { 91 | return this._measurement 92 | } 93 | 94 | /** 95 | * Stage of the current measure or undefined if no active measure. 96 | */ 97 | get stage (): MeasureStage | undefined { 98 | return this._flow?.stage 99 | } 100 | 101 | constructor (viewer: Viewer) { 102 | this._viewer = viewer 103 | } 104 | 105 | /** 106 | * Starts a new measure flow where the two next click are overriden. 107 | * Currently running flow if any will be aborted. 108 | * Promise is resolved if flow is succesfully completed, rejected otherwise. 109 | * Do not override viewer.onMouseClick while this flow is active. 110 | */ 111 | async start (onProgress?: () => void) { 112 | this.abort() 113 | 114 | this._flow = new MeasureFlow(this) 115 | this._previousScheme = this._viewer.inputs.scheme 116 | this._viewer.inputs.scheme = this._flow 117 | this._flow.onProgress = () => onProgress?.() 118 | 119 | return new Promise((resolve, reject) => { 120 | if (this._flow) { 121 | this._flow.onComplete = (success: boolean) => { 122 | if (this._previousScheme) { 123 | this._viewer.inputs.scheme = this._previousScheme 124 | this._previousScheme = undefined 125 | } 126 | if (success) resolve() 127 | else { 128 | reject(new Error('Measurement Aborted')) 129 | } 130 | } 131 | } 132 | }) 133 | } 134 | 135 | /** 136 | * Should be private. 137 | */ 138 | onFirstClick (action: InputAction) { 139 | this.clear() 140 | this._meshes = new MeasureGizmo(this._viewer) 141 | this._startPos = action.raycast.position 142 | if (this._startPos) { 143 | this._meshes.start(this._startPos) 144 | } 145 | } 146 | 147 | /** 148 | * Should be private. 149 | */ 150 | onMouseMove () { 151 | this._meshes?.hide() 152 | } 153 | 154 | /** 155 | * Should be private. 156 | */ 157 | onMouseIdle (action: InputAction) { 158 | // Show markers and line on hit 159 | if (!action) { 160 | this._meshes?.hide() 161 | return 162 | } 163 | const position = action.raycast.position 164 | if (position && this._startPos) { 165 | this._measurement = action.object 166 | ? position.clone().sub(this._startPos) 167 | : undefined 168 | } 169 | 170 | if (action.object && position && this._startPos) { 171 | this._meshes?.update(this._startPos, position) 172 | } else { 173 | this._meshes?.hide() 174 | } 175 | } 176 | 177 | /** 178 | * Should be private. 179 | */ 180 | onSecondClick (action: InputAction) { 181 | if (!action.object || !this._startPos) { 182 | return false 183 | } 184 | 185 | // Compute measurement vector component 186 | this._endPos = action.raycast.position 187 | if (!this._endPos) return false 188 | 189 | this._measurement = this._endPos.clone().sub(this._startPos) 190 | console.log(`Distance: ${this._measurement.length()}`) 191 | console.log( 192 | ` 193 | X: ${this._measurement.x}, 194 | Y: ${this._measurement.y}, 195 | Z: ${this._measurement.z} 196 | ` 197 | ) 198 | this._meshes?.finish(this._startPos, this._endPos) 199 | 200 | return true 201 | } 202 | 203 | /** 204 | * Aborts the current measure flow, fails the related promise. 205 | */ 206 | abort () { 207 | this._flow?.abort() 208 | this._flow = undefined 209 | 210 | this._startPos = undefined 211 | this._endPos = undefined 212 | this._measurement = undefined 213 | } 214 | 215 | /** 216 | * Clears meshes. 217 | */ 218 | clear () { 219 | this._meshes?.dispose() 220 | this._meshes = undefined 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/measure/measureFlow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | @module viw-webgl-viewer/gizmos/measure 3 | */ 4 | 5 | import { InputScheme } from '../../inputs/input' 6 | import { InputAction } from '../../raycaster' 7 | import { Measure } from './measure' 8 | 9 | export type MeasureStage = 'ready' | 'active' | 'done' | 'failed' 10 | 11 | /** 12 | * Inputs scheme for measuring as a small state machine. 13 | */ 14 | export class MeasureFlow implements InputScheme { 15 | private readonly _gizmoMeasure: Measure 16 | private _stage: MeasureStage | undefined 17 | private removeMouseListener: (() => void) | undefined 18 | 19 | constructor (gizmoMeasure: Measure) { 20 | this._gizmoMeasure = gizmoMeasure 21 | this._stage = 'ready' 22 | } 23 | 24 | onProgress: ((stage: MeasureStage) => void) | undefined 25 | onComplete: ((success: boolean) => void) | undefined 26 | 27 | get stage () { 28 | return this._stage 29 | } 30 | 31 | private unregister () { 32 | this.removeMouseListener?.() 33 | this.removeMouseListener = undefined 34 | } 35 | 36 | /** 37 | * Cancels current measuring flow. 38 | */ 39 | abort () { 40 | if (this.stage === 'active' || this.stage === 'ready') { 41 | this._stage = undefined 42 | this.onComplete?.(false) 43 | this.unregister() 44 | } 45 | } 46 | 47 | /** 48 | * Implementation for InputScheme onMainAction 49 | */ 50 | onMainAction (action: InputAction) { 51 | switch (this._stage) { 52 | case 'ready': 53 | if (!action.object) return 54 | this._gizmoMeasure.onFirstClick(action) 55 | this._stage = 'active' 56 | this.onProgress?.(this._stage) 57 | break 58 | case 'active': 59 | this._stage = this._gizmoMeasure.onSecondClick(action) 60 | ? 'done' 61 | : 'failed' 62 | this.onProgress?.(this._stage) 63 | this.onComplete?.(this._stage === 'done') 64 | this.unregister() 65 | break 66 | } 67 | } 68 | 69 | /** 70 | * Implementation for InputScheme onIdleAction 71 | */ 72 | onIdleAction (action: InputAction) { 73 | if (this._stage === 'active') this._gizmoMeasure.onMouseIdle(action) 74 | } 75 | 76 | /** 77 | * Implementation for InputScheme onKeyAction 78 | */ 79 | onKeyAction (key: number) { 80 | return false 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/measure/measureHtml.ts: -------------------------------------------------------------------------------- 1 | /** 2 | @module viw-webgl-viewer/gizmos/measure 3 | */ 4 | 5 | /** 6 | * Different styles of measure display. 7 | */ 8 | export type MeasureStyle = 'all' | 'Dist' | 'X' | 'Y' | 'Z' 9 | 10 | /** 11 | * Structure of the html element used for measure. 12 | */ 13 | export type MeasureElement = { 14 | div: HTMLElement 15 | value: HTMLTableCellElement | undefined 16 | values: { 17 | dist: HTMLTableCellElement | undefined 18 | x: HTMLTableCellElement | undefined 19 | y: HTMLTableCellElement | undefined 20 | z: HTMLTableCellElement | undefined 21 | } 22 | } 23 | 24 | /** 25 | * Creates a html structure for measure value overlays 26 | * It either creates a single rows or all rows depending on style 27 | * Structure is a Table of Label:Value 28 | */ 29 | export function createMeasureElement (style: MeasureStyle): MeasureElement { 30 | const div = document.createElement('div') 31 | div.className = 'vim-measure' 32 | 33 | const table = document.createElement('table') 34 | div.appendChild(table) 35 | 36 | let distValue: HTMLTableCellElement | undefined 37 | let xValue: HTMLTableCellElement | undefined 38 | let yValue: HTMLTableCellElement | undefined 39 | let zValue: HTMLTableCellElement | undefined 40 | 41 | if (style === 'all' || style === 'Dist') { 42 | const trDist = document.createElement('tr') 43 | const tdDistLabel = document.createElement('td') 44 | const tdDistValue = document.createElement('td') 45 | 46 | table.appendChild(trDist) 47 | trDist.appendChild(tdDistLabel) 48 | trDist.appendChild(tdDistValue) 49 | 50 | tdDistLabel.className = 'vim-measure-label-d' 51 | tdDistValue.className = 'vim-measure-value-d' 52 | 53 | tdDistLabel.textContent = 'Dist' 54 | distValue = tdDistValue 55 | } 56 | 57 | if (style === 'all' || style === 'X') { 58 | const trX = document.createElement('tr') 59 | const tdXLabel = document.createElement('td') 60 | const tdXValue = document.createElement('td') 61 | 62 | table.appendChild(trX) 63 | trX.appendChild(tdXLabel) 64 | trX.appendChild(tdXValue) 65 | 66 | tdXLabel.className = 'vim-measure-label-x' 67 | tdXValue.className = 'vim-measure-value-x' 68 | 69 | tdXLabel.textContent = 'X' 70 | xValue = tdXValue 71 | } 72 | 73 | if (style === 'all' || style === 'Y') { 74 | const trY = document.createElement('tr') 75 | const tdYLabel = document.createElement('td') 76 | const tdYValue = document.createElement('td') 77 | 78 | table.appendChild(trY) 79 | trY.appendChild(tdYLabel) 80 | trY.appendChild(tdYValue) 81 | 82 | tdYLabel.className = 'vim-measure-label-y' 83 | tdYValue.className = 'vim-measure-value-y' 84 | 85 | tdYLabel.textContent = 'Y' 86 | yValue = tdYValue 87 | } 88 | 89 | if (style === 'all' || style === 'Z') { 90 | const trZ = document.createElement('tr') 91 | const tdZLabel = document.createElement('td') 92 | const tdZValue = document.createElement('td') 93 | 94 | table.appendChild(trZ) 95 | trZ.appendChild(tdZLabel) 96 | trZ.appendChild(tdZValue) 97 | 98 | tdZLabel.className = 'vim-measure-label-z' 99 | tdZValue.className = 'vim-measure-value-z' 100 | tdZLabel.textContent = 'Z' 101 | zValue = tdZValue 102 | } 103 | 104 | return { 105 | div, 106 | value: 107 | style === 'Dist' 108 | ? distValue 109 | : style === 'X' 110 | ? xValue 111 | : style === 'Y' 112 | ? yValue 113 | : style === 'Z' 114 | ? zValue 115 | : undefined, 116 | values: { dist: distValue, x: xValue, y: yValue, z: zValue } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/sectionBox/sectionBox.ts: -------------------------------------------------------------------------------- 1 | /** 2 | @module viw-webgl-viewer/gizmos/sectionBox 3 | */ 4 | 5 | import { Viewer } from '../../viewer' 6 | import * as THREE from 'three' 7 | import { BoxMesh, BoxOutline, BoxHighlight } from './sectionBoxGizmo' 8 | import { BoxInputs } from './sectionBoxInputs' 9 | import { SignalDispatcher } from 'ste-signals' 10 | import { SimpleEventDispatcher } from 'ste-simple-events' 11 | 12 | /** 13 | * Gizmo for section box, serving as a proxy between the renderer and the user. 14 | */ 15 | export class SectionBox { 16 | // dependencies 17 | private _viewer: Viewer 18 | 19 | // resources 20 | private _inputs: BoxInputs 21 | private _cube: BoxMesh 22 | private _outline: BoxOutline 23 | private _highlight: BoxHighlight 24 | 25 | // State 26 | private _normal: THREE.Vector3 27 | private _clip: boolean | undefined = undefined 28 | private _visible: boolean | undefined = undefined 29 | private _interactive: boolean | undefined = undefined 30 | 31 | private _onStateChanged = new SignalDispatcher() 32 | private _onBoxConfirm = new SimpleEventDispatcher() 33 | private _onHover = new SimpleEventDispatcher() 34 | 35 | /** 36 | * Signal dispatched when clip, show, or interactive are updated. 37 | */ 38 | get onStateChanged () { 39 | return this._onStateChanged.asEvent() 40 | } 41 | 42 | /** 43 | * Signal dispatched when user is done manipulating the box. 44 | */ 45 | get onBoxConfirm () { 46 | return this._onBoxConfirm.asEvent() 47 | } 48 | 49 | /** 50 | * Signal dispatched with true when pointer enters box and false when pointer leaves. 51 | */ 52 | get onHover () { 53 | return this._onHover.asEvent() 54 | } 55 | 56 | private get renderer () { 57 | return this._viewer.renderer 58 | } 59 | 60 | private get section () { 61 | return this._viewer.renderer.section 62 | } 63 | 64 | constructor (viewer: Viewer) { 65 | this._viewer = viewer 66 | 67 | this._normal = new THREE.Vector3() 68 | 69 | this._cube = new BoxMesh() 70 | this._outline = new BoxOutline() 71 | this._highlight = new BoxHighlight() 72 | 73 | this.renderer.add(this._cube) 74 | this.renderer.add(this._outline) 75 | this.renderer.add(this._highlight) 76 | 77 | this._inputs = new BoxInputs( 78 | viewer, 79 | this._cube, 80 | this._viewer.renderer.section.box 81 | ) 82 | this._inputs.onFaceEnter = (normal) => { 83 | this._normal = normal 84 | if (this.visible) this._highlight.highlight(this.section.box, normal) 85 | this._onHover.dispatch(normal.x !== 0 || normal.y !== 0 || normal.z !== 0) 86 | this.renderer.needsUpdate = true 87 | } 88 | 89 | this._inputs.onBoxStretch = (box) => { 90 | this.renderer.section.fitBox(box) 91 | this.update() 92 | } 93 | this._inputs.onBoxConfirm = (box) => this._onBoxConfirm.dispatch(box) 94 | 95 | this.clip = false 96 | this.visible = false 97 | this.interactive = false 98 | this.update() 99 | } 100 | 101 | /** 102 | * Section bounding box, to update the box use fitBox. 103 | */ 104 | get box () { 105 | return this.section.box 106 | } 107 | 108 | /** 109 | * Determines whether the section gizmo will section the model with clipping planes. 110 | */ 111 | get clip () { 112 | return this._clip ?? false 113 | } 114 | 115 | set clip (value: boolean) { 116 | if (value === this._clip) return 117 | this._clip = value 118 | this.renderer.section.active = value 119 | this._onStateChanged.dispatch() 120 | } 121 | 122 | /** 123 | * Determines whether the gizmo reacts to user inputs. 124 | */ 125 | get interactive () { 126 | return this._interactive ?? false 127 | } 128 | 129 | set interactive (value: boolean) { 130 | if (value === this._interactive) return 131 | if (!this._interactive && value) this._inputs.register() 132 | if (this._interactive && !value) this._inputs.unregister() 133 | this._interactive = value 134 | this._highlight.visible = false 135 | this.renderer.needsUpdate = true 136 | this._onStateChanged.dispatch() 137 | } 138 | 139 | /** 140 | * Determines whether the gizmo will be rendered. 141 | */ 142 | get visible () { 143 | return this._visible ?? false 144 | } 145 | 146 | set visible (value: boolean) { 147 | if (value === this._visible) return 148 | this._visible = value 149 | this._cube.visible = value 150 | this._outline.visible = value 151 | this._highlight.visible = value 152 | if (value) this.update() 153 | this.renderer.needsUpdate = true 154 | this._onStateChanged.dispatch() 155 | } 156 | 157 | /** 158 | * Sets the section gizmo size to match the given box. 159 | * @param {THREE.Box3} box - The box to match the section gizmo size to. 160 | * @param {number} [padding=1] - The padding to apply to the box. 161 | */ 162 | public fitBox (box: THREE.Box3, padding = 1) { 163 | if (!box) return 164 | const b = box.expandByScalar(padding) 165 | this._cube.fitBox(b) 166 | this._outline.fitBox(b) 167 | this.renderer.section.fitBox(b) 168 | this._onBoxConfirm.dispatch(this.box) 169 | this.renderer.needsUpdate = true 170 | } 171 | 172 | /** 173 | * Call this if there were direct changes to renderer.section 174 | */ 175 | update () { 176 | this.fitBox(this.section.box, 0) 177 | this._highlight.highlight(this.section.box, this._normal) 178 | this.renderer.needsUpdate = true 179 | } 180 | 181 | /** 182 | * Removes gizmo from rendering and inputs and dispose all resources. 183 | */ 184 | dispose () { 185 | this.renderer.remove(this._cube) 186 | this.renderer.remove(this._outline) 187 | this.renderer.remove(this._highlight) 188 | 189 | this._inputs.unregister() 190 | this._cube.dispose() 191 | this._outline.dispose() 192 | this._highlight.dispose() 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/gizmos/sectionBox/sectionBoxGizmo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/gizmos/sectionBox 3 | */ 4 | 5 | import * as THREE from 'three' 6 | 7 | /** 8 | * Defines the thin outline on the edges of the section box. 9 | */ 10 | export class BoxOutline extends THREE.LineSegments { 11 | constructor () { 12 | // prettier-ignore 13 | const vertices = new Float32Array([ 14 | -0.5, -0.5, -0.5, 15 | 0.5, -0.5, -0.5, 16 | 0.5, 0.5, -0.5, 17 | -0.5, 0.5, -0.5, 18 | -0.5, -0.5, 0.5, 19 | 0.5, -0.5, 0.5, 20 | 0.5, 0.5, 0.5, 21 | -0.5, 0.5, 0.5 22 | ]) 23 | // prettier-ignore 24 | const indices = [ 25 | 26 | 0.5, 1, 27 | 1, 2, 28 | 2, 3, 29 | 3, 0, 30 | 31 | 4, 5, 32 | 5, 6, 33 | 6, 7, 34 | 7, 4, 35 | 36 | 0, 4, 37 | 1, 5, 38 | 2, 6, 39 | 3, 7 40 | ] 41 | const geo = new THREE.BufferGeometry() 42 | const mat = new THREE.LineBasicMaterial({ 43 | opacity: 1, 44 | color: new THREE.Color(0x000000) 45 | }) 46 | geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3)) 47 | geo.setIndex(indices) 48 | super(geo, mat) 49 | } 50 | 51 | /** 52 | * Resize the outline to the given box. 53 | */ 54 | fitBox (box: THREE.Box3) { 55 | this.scale.set( 56 | box.max.x - box.min.x, 57 | box.max.y - box.min.y, 58 | box.max.z - box.min.z 59 | ) 60 | this.position.set( 61 | (box.max.x + box.min.x) / 2, 62 | (box.max.y + box.min.y) / 2, 63 | (box.max.z + box.min.z) / 2 64 | ) 65 | } 66 | 67 | /** 68 | * Disposes of all resources. 69 | */ 70 | dispose () { 71 | this.geometry.dispose() 72 | ;(this.material as THREE.Material).dispose() 73 | } 74 | } 75 | 76 | /** 77 | * Defines the box mesh for the section box. 78 | */ 79 | export class BoxMesh extends THREE.Mesh { 80 | constructor () { 81 | const geo = new THREE.BoxGeometry() 82 | const mat = new THREE.MeshBasicMaterial({ 83 | opacity: 0.3, 84 | transparent: true, 85 | color: new THREE.Color(0x0050bb), 86 | depthTest: false 87 | }) 88 | 89 | super(geo, mat) 90 | } 91 | 92 | /** 93 | * Resize the mesh to the given box. 94 | */ 95 | fitBox (box: THREE.Box3) { 96 | this.scale.set( 97 | box.max.x - box.min.x, 98 | box.max.y - box.min.y, 99 | box.max.z - box.min.z 100 | ) 101 | this.position.set( 102 | (box.max.x + box.min.x) / 2, 103 | (box.max.y + box.min.y) / 2, 104 | (box.max.z + box.min.z) / 2 105 | ) 106 | } 107 | 108 | /** 109 | * Disposes of all resources. 110 | */ 111 | dispose () { 112 | this.geometry.dispose() 113 | ;(this.material as THREE.Material).dispose() 114 | } 115 | } 116 | 117 | /** 118 | * Defines the face highlight on hover for the section box. 119 | */ 120 | export class BoxHighlight extends THREE.Mesh { 121 | constructor () { 122 | const geo = new THREE.BufferGeometry() 123 | geo.setAttribute( 124 | 'position', 125 | new THREE.BufferAttribute(new Float32Array(12), 3) 126 | ) 127 | geo.setIndex([0, 1, 2, 0, 2, 3]) 128 | 129 | const mat = new THREE.MeshBasicMaterial({ 130 | opacity: 0.7, 131 | transparent: true, 132 | depthTest: false, 133 | side: THREE.DoubleSide 134 | }) 135 | super(geo, mat) 136 | this.renderOrder = 1 137 | // Because position is always (0,0,0) 138 | this.frustumCulled = false 139 | } 140 | 141 | /** 142 | * Sets the face to highlight 143 | * @param normal a direction vector from theses options (X,-X, Y,-Y, Z,-Z) 144 | */ 145 | highlight (box: THREE.Box3, normal: THREE.Vector3) { 146 | this.visible = false 147 | const positions = this.geometry.getAttribute('position') 148 | 149 | if (normal.x > 0.1) { 150 | positions.setXYZ(0, box.max.x, box.max.y, box.max.z) 151 | positions.setXYZ(1, box.max.x, box.min.y, box.max.z) 152 | positions.setXYZ(2, box.max.x, box.min.y, box.min.z) 153 | positions.setXYZ(3, box.max.x, box.max.y, box.min.z) 154 | this.visible = true 155 | } 156 | if (normal.x < -0.1) { 157 | positions.setXYZ(0, box.min.x, box.max.y, box.max.z) 158 | positions.setXYZ(1, box.min.x, box.min.y, box.max.z) 159 | positions.setXYZ(2, box.min.x, box.min.y, box.min.z) 160 | positions.setXYZ(3, box.min.x, box.max.y, box.min.z) 161 | this.visible = true 162 | } 163 | if (normal.y > 0.1) { 164 | positions.setXYZ(0, box.max.x, box.max.y, box.max.z) 165 | positions.setXYZ(1, box.min.x, box.max.y, box.max.z) 166 | positions.setXYZ(2, box.min.x, box.max.y, box.min.z) 167 | positions.setXYZ(3, box.max.x, box.max.y, box.min.z) 168 | this.visible = true 169 | } 170 | if (normal.y < -0.1) { 171 | positions.setXYZ(0, box.max.x, box.min.y, box.max.z) 172 | positions.setXYZ(1, box.min.x, box.min.y, box.max.z) 173 | positions.setXYZ(2, box.min.x, box.min.y, box.min.z) 174 | positions.setXYZ(3, box.max.x, box.min.y, box.min.z) 175 | this.visible = true 176 | } 177 | if (normal.z > 0.1) { 178 | positions.setXYZ(0, box.max.x, box.max.y, box.max.z) 179 | positions.setXYZ(1, box.min.x, box.max.y, box.max.z) 180 | positions.setXYZ(2, box.min.x, box.min.y, box.max.z) 181 | positions.setXYZ(3, box.max.x, box.min.y, box.max.z) 182 | this.visible = true 183 | } 184 | if (normal.z < -0.1) { 185 | positions.setXYZ(0, box.max.x, box.max.y, box.min.z) 186 | positions.setXYZ(1, box.min.x, box.max.y, box.min.z) 187 | positions.setXYZ(2, box.min.x, box.min.y, box.min.z) 188 | positions.setXYZ(3, box.max.x, box.min.y, box.min.z) 189 | this.visible = true 190 | } 191 | positions.needsUpdate = true 192 | } 193 | 194 | /** 195 | * Disposes all resources. 196 | */ 197 | dispose () { 198 | this.geometry.dispose() 199 | ;(this.material as THREE.Material).dispose() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/inputs/inputHandler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/inputs 3 | */ 4 | 5 | import { Viewer } from '../viewer' 6 | 7 | /** 8 | * Base class for various input handlers. 9 | * It provides convenience to register to and unregister from events. 10 | */ 11 | export class InputHandler { 12 | protected _viewer: Viewer 13 | protected _unregisters: Function[] = [] 14 | 15 | constructor (viewer: Viewer) { 16 | this._viewer = viewer 17 | } 18 | 19 | protected reg = ( 20 | // eslint-disable-next-line no-undef 21 | handler: Document | HTMLElement | Window, 22 | type: string, 23 | listener: (event: any) => void 24 | ) => { 25 | handler.addEventListener(type, listener) 26 | this._unregisters.push(() => handler.removeEventListener(type, listener)) 27 | } 28 | 29 | /** 30 | * Register handler to related browser events 31 | * Prevents double registrations 32 | */ 33 | register () { 34 | if (this._unregisters.length > 0) return 35 | this.addListeners() 36 | } 37 | 38 | protected addListeners () {} 39 | 40 | /** 41 | * Unregister handler from related browser events 42 | * Prevents double unregistration 43 | */ 44 | unregister () { 45 | this._unregisters.forEach((f) => f()) 46 | this._unregisters.length = 0 47 | this.reset() 48 | } 49 | 50 | /** 51 | * Reset handler states such as button down, drag, etc. 52 | */ 53 | reset () {} 54 | } 55 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/inputs/keyboard.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/inputs 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { InputHandler } from './inputHandler' 7 | 8 | /** 9 | * Key values for viewer 10 | */ 11 | export const KEYS = { 12 | KEY_0: 48, 13 | KEY_1: 49, 14 | KEY_2: 50, 15 | KEY_3: 51, 16 | KEY_4: 52, 17 | KEY_5: 53, 18 | KEY_6: 54, 19 | KEY_7: 55, 20 | KEY_8: 56, 21 | KEY_9: 57, 22 | 23 | KEY_LEFT: 0x25, 24 | KEY_RIGHT: 0x27, 25 | KEY_UP: 0x26, 26 | KEY_DOWN: 0x28, 27 | KEY_CTRL: 0x11, 28 | KEY_SHIFT: 0x10, 29 | KEY_ENTER: 0x0d, 30 | KEY_SPACE: 0x20, 31 | KEY_TAB: 0x09, 32 | KEY_ESCAPE: 0x1b, 33 | KEY_BACKSPACE: 0x08, 34 | KEY_HOME: 0x24, 35 | KEY_END: 0x23, 36 | KEY_INSERT: 0x2d, 37 | KEY_DELETE: 0x2e, 38 | KEY_ALT: 0x12, 39 | 40 | KEY_F1: 0x70, 41 | KEY_F2: 0x71, 42 | KEY_F3: 0x72, 43 | KEY_F4: 0x73, 44 | KEY_F5: 0x74, 45 | KEY_F6: 0x75, 46 | KEY_F7: 0x76, 47 | KEY_F8: 0x77, 48 | KEY_F9: 0x78, 49 | KEY_F10: 0x79, 50 | KEY_F11: 0x7a, 51 | KEY_F12: 0x7b, 52 | 53 | KEY_NUMPAD0: 0x60, 54 | KEY_NUMPAD1: 0x61, 55 | KEY_NUMPAD2: 0x62, 56 | KEY_NUMPAD3: 0x63, 57 | KEY_NUMPAD4: 0x64, 58 | KEY_NUMPAD5: 0x65, 59 | KEY_NUMPAD6: 0x66, 60 | KEY_NUMPAD7: 0x67, 61 | KEY_NUMPAD8: 0x68, 62 | KEY_NUMPAD9: 0x69, 63 | 64 | KEY_ADD: 0x6b, 65 | KEY_SUBTRACT: 0x6d, 66 | KEY_MULTIPLY: 0x6a, 67 | KEY_DIVIDE: 0x6f, 68 | KEY_SEPARATOR: 0x6c, 69 | KEY_DECIMAL: 0x6e, 70 | 71 | KEY_OEM_PLUS: 0xbb, 72 | KEY_OEM_MINUS: 0xbd, 73 | 74 | KEY_A: 65, 75 | KEY_B: 66, 76 | KEY_C: 67, 77 | KEY_D: 68, 78 | KEY_E: 69, 79 | KEY_F: 70, 80 | KEY_G: 71, 81 | KEY_H: 72, 82 | KEY_I: 73, 83 | KEY_J: 74, 84 | KEY_K: 75, 85 | KEY_L: 76, 86 | KEY_M: 77, 87 | KEY_N: 78, 88 | KEY_O: 79, 89 | KEY_P: 80, 90 | KEY_Q: 81, 91 | KEY_R: 82, 92 | KEY_S: 83, 93 | KEY_T: 84, 94 | KEY_U: 85, 95 | KEY_V: 86, 96 | KEY_W: 87, 97 | KEY_X: 88, 98 | KEY_Y: 89, 99 | KEY_Z: 90 100 | } 101 | const KeySet = new Set(Object.values(KEYS)) 102 | 103 | /** 104 | * Manages keyboard user inputs 105 | */ 106 | export class KeyboardHandler extends InputHandler { 107 | // Settings 108 | private SHIFT_MULTIPLIER: number = 3.0 109 | 110 | // State 111 | isUpPressed: boolean = false 112 | isDownPressed: boolean = false 113 | isLeftPressed: boolean = false 114 | isRightPressed: boolean = false 115 | isEPressed: boolean = false 116 | isQPressed: boolean = false 117 | isShiftPressed: boolean = false 118 | isCtrlPressed: boolean = false 119 | arrowsEnabled: boolean = true 120 | 121 | protected override addListeners (): void { 122 | this.reg(document, 'keydown', (e) => this.onKeyDown(e)) 123 | this.reg(document, 'keyup', (e) => this.onKeyUp(e)) 124 | this.reg(this._viewer.viewport.canvas, 'focusout', () => this.reset()) 125 | this.reg(window, 'resize', () => this.reset()) 126 | } 127 | 128 | override reset () { 129 | this.isUpPressed = false 130 | this.isDownPressed = false 131 | this.isLeftPressed = false 132 | this.isRightPressed = false 133 | this.isEPressed = false 134 | this.isQPressed = false 135 | this.isShiftPressed = false 136 | this.isCtrlPressed = false 137 | this.applyMove() 138 | } 139 | 140 | private get camera () { 141 | return this._viewer.camera 142 | } 143 | 144 | private onKeyUp (event: KeyboardEvent) { 145 | this.onKey(event, false) 146 | } 147 | 148 | private onKeyDown (event: KeyboardEvent) { 149 | this.onKey(event, true) 150 | } 151 | 152 | private onKey (event: KeyboardEvent, keyDown: boolean) { 153 | // Buttons that activate once on key up 154 | if (!keyDown && KeySet.has(event.keyCode)) { 155 | if (this._viewer.inputs.KeyAction(event.keyCode)) { 156 | event.preventDefault() 157 | } 158 | } 159 | 160 | // Camera Movement, Buttons that need constant state refresh 161 | switch (event.keyCode) { 162 | case KEYS.KEY_W: 163 | case KEYS.KEY_UP: 164 | this.isUpPressed = keyDown 165 | this.applyMove() 166 | event.preventDefault() 167 | break 168 | case KEYS.KEY_S: 169 | case KEYS.KEY_DOWN: 170 | this.isDownPressed = keyDown 171 | this.applyMove() 172 | event.preventDefault() 173 | break 174 | case KEYS.KEY_D: 175 | case KEYS.KEY_RIGHT: 176 | this.isRightPressed = keyDown 177 | this.applyMove() 178 | event.preventDefault() 179 | break 180 | case KEYS.KEY_A: 181 | case KEYS.KEY_LEFT: 182 | this.isLeftPressed = keyDown 183 | this.applyMove() 184 | event.preventDefault() 185 | break 186 | case KEYS.KEY_E: 187 | this.isEPressed = keyDown 188 | this.applyMove() 189 | event.preventDefault() 190 | break 191 | case KEYS.KEY_Q: 192 | this.isQPressed = keyDown 193 | this.applyMove() 194 | event.preventDefault() 195 | break 196 | case KEYS.KEY_SHIFT: 197 | this.isShiftPressed = keyDown 198 | this.applyMove() 199 | event.preventDefault() 200 | break 201 | case KEYS.KEY_CTRL: 202 | this.isCtrlPressed = keyDown 203 | event.preventDefault() 204 | break 205 | } 206 | } 207 | 208 | private applyMove () { 209 | const move = new THREE.Vector3( 210 | (this.isRightPressed ? 1 : 0) - (this.isLeftPressed ? 1 : 0), 211 | (this.isEPressed ? 1 : 0) - (this.isQPressed ? 1 : 0), 212 | (this.isUpPressed ? 1 : 0) - (this.isDownPressed ? 1 : 0) 213 | ) 214 | const speed = this.isShiftPressed ? this.SHIFT_MULTIPLIER : 1 215 | move.multiplyScalar(speed) 216 | if (this.arrowsEnabled) { 217 | this.camera.localVelocity = move 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/rendering/mergePass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/rendering 3 | */ 4 | 5 | import THREE from 'three' 6 | import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass' 7 | import { ViewerMaterials } from '../../vim-loader/materials/viewerMaterials' 8 | import { MergeMaterial } from '../../vim-loader/materials/mergeMaterial' 9 | 10 | /** 11 | * Merges a source buffer into the the current write buffer. 12 | */ 13 | export class MergePass extends Pass { 14 | private _fsQuad: FullScreenQuad 15 | private _material: MergeMaterial 16 | 17 | constructor (source: THREE.Texture, materials?: ViewerMaterials) { 18 | super() 19 | 20 | this._fsQuad = new FullScreenQuad() 21 | this._material = materials?.merge ?? new MergeMaterial() 22 | this._fsQuad.material = this._material.material 23 | this._material.sourceA = source 24 | } 25 | 26 | dispose () { 27 | this._fsQuad.dispose() 28 | } 29 | 30 | render ( 31 | renderer: THREE.WebGLRenderer, 32 | writeBuffer: THREE.WebGLRenderTarget, 33 | readBuffer: THREE.WebGLRenderTarget 34 | ) { 35 | this._material.sourceB = readBuffer.texture 36 | // 2. Draw the outlines using the depth texture and normal texture 37 | // and combine it with the scene color 38 | if (this.renderToScreen) { 39 | // If this is the last effect, then renderToScreen is true. 40 | // So we should render to the screen by setting target null 41 | // Otherwise, just render into the writeBuffer that the next effect will use as its read buffer. 42 | renderer.setRenderTarget(null) 43 | this._fsQuad.render(renderer) 44 | } else { 45 | renderer.setRenderTarget(writeBuffer) 46 | this._fsQuad.render(renderer) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/rendering/outlinePass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/rendering 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js' 7 | import { OutlineMaterial } from '../../vim-loader/materials/outlineMaterial' 8 | 9 | // Follows the structure of 10 | // https://github.com/mrdoob/three.js/blob/master/examples/jsm/postprocessing/OutlinePass.js 11 | // Based on https://github.com/OmarShehata/webgl-outlines/blob/cf81030d6f2bc20e6113fbf6cfd29170064dce48/threejs/src/CustomOutlinePass.js 12 | /** 13 | * Edge detection pass on the current readbuffer depth texture. 14 | */ 15 | export class OutlinePass extends Pass { 16 | private _fsQuad: FullScreenQuad 17 | material: OutlineMaterial 18 | 19 | constructor ( 20 | sceneBuffer: THREE.Texture, 21 | camera: THREE.PerspectiveCamera | THREE.OrthographicCamera, 22 | material?: OutlineMaterial 23 | ) { 24 | super() 25 | 26 | this.material = material ?? new OutlineMaterial() 27 | this.material.sceneBuffer = sceneBuffer 28 | this.material.camera = camera 29 | this._fsQuad = new FullScreenQuad(this.material.material) 30 | } 31 | 32 | setSize (width: number, height: number) { 33 | this.material.resolution = new THREE.Vector2(width, height) 34 | } 35 | 36 | get camera () { 37 | return this.material.camera 38 | } 39 | 40 | set camera (value: THREE.PerspectiveCamera | THREE.OrthographicCamera) { 41 | this.material.camera = value 42 | } 43 | 44 | dispose () { 45 | this._fsQuad.dispose() 46 | this.material.dispose() 47 | } 48 | 49 | render ( 50 | renderer: THREE.WebGLRenderer, 51 | writeBuffer: THREE.WebGLRenderTarget, 52 | readBuffer: THREE.WebGLRenderTarget 53 | ) { 54 | // Turn off writing to the depth buffer 55 | // because we need to read from it in the subsequent passes. 56 | const depthBufferValue = writeBuffer.depthBuffer 57 | writeBuffer.depthBuffer = false 58 | this.material.depthBuffer = readBuffer.depthTexture 59 | 60 | // 2. Draw the outlines using the depth texture and normal texture 61 | // and combine it with the scene color 62 | if (this.renderToScreen) { 63 | // If this is the last effect, then renderToScreen is true. 64 | // So we should render to the screen by setting target null 65 | // Otherwise, just render into the writeBuffer that the next effect will use as its read buffer. 66 | renderer.setRenderTarget(null) 67 | this._fsQuad.render(renderer) 68 | } else { 69 | renderer.setRenderTarget(writeBuffer) 70 | this._fsQuad.render(renderer) 71 | } 72 | 73 | // Reset the depthBuffer value so we continue writing to it in the next render. 74 | writeBuffer.depthBuffer = depthBufferValue 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/rendering/renderScene.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/rendering 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { Scene } from '../../vim-loader/scene' 7 | import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' 8 | 9 | /** 10 | * Wrapper around the THREE scene that tracks bounding box and other information. 11 | */ 12 | export class RenderScene { 13 | scene: THREE.Scene 14 | 15 | // state 16 | boxUpdated = false 17 | 18 | private _vimScenes: Scene[] = [] 19 | private _boundingBox: THREE.Box3 | undefined 20 | private _memory = 0 21 | private _2dCount = 0 22 | 23 | constructor () { 24 | this.scene = new THREE.Scene() 25 | } 26 | 27 | get estimatedMemory () { 28 | return this._memory 29 | } 30 | 31 | has2dObjects () { 32 | return this._2dCount > 0 33 | } 34 | 35 | hasOutline () { 36 | for (const s of this._vimScenes) { 37 | if (s.hasOutline) return true 38 | } 39 | return false 40 | } 41 | 42 | /** Clears the scene updated flags */ 43 | clearUpdateFlags () { 44 | this._vimScenes.forEach((s) => s.clearUpdateFlag()) 45 | } 46 | 47 | /** 48 | * Returns the bounding box encompasing all rendererd objects. 49 | * @param target box in which to copy result, a new instance is created if undefined. 50 | */ 51 | getBoundingBox (target: THREE.Box3 = new THREE.Box3()) { 52 | return this._boundingBox 53 | ? target.copy(this._boundingBox) 54 | : target.set(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1)) 55 | } 56 | 57 | /** 58 | * Returns the bounding box of the average center of all meshes. 59 | * Less precise but is more stable against outliers. 60 | */ 61 | getAverageBoundingBox () { 62 | if (this._vimScenes.length === 0) { 63 | return new THREE.Box3() 64 | } 65 | const result = new THREE.Box3() 66 | result.copy(this._vimScenes[0].getAverageBoundingBox()) 67 | for (let i = 1; i < this._vimScenes.length; i++) { 68 | result.union(this._vimScenes[i].getAverageBoundingBox()) 69 | } 70 | return result 71 | } 72 | 73 | /** 74 | * Add object to be rendered 75 | */ 76 | add (target: Scene | THREE.Object3D) { 77 | if (target instanceof Scene) { 78 | this.addScene(target) 79 | return 80 | } 81 | 82 | this._2dCount += this.count2dObjects(target) 83 | this.scene.add(target) 84 | } 85 | 86 | private count2dObjects (target : THREE.Object3D) { 87 | if (target instanceof CSS2DObject) { 88 | return 1 89 | } 90 | if (target instanceof THREE.Group) { 91 | let result = 0 92 | for (const child of target.children) { 93 | if (child instanceof CSS2DObject) { 94 | result++ 95 | } 96 | } 97 | return result 98 | } 99 | return 0 100 | } 101 | 102 | private unparent2dObjects (target : THREE.Object3D) { 103 | // A quirk of css2d object is they need to be removed individually. 104 | if (target instanceof THREE.Group) { 105 | for (const child of target.children) { 106 | if (child instanceof CSS2DObject) { 107 | target.remove(child) 108 | } 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Remove object from rendering 115 | */ 116 | remove (target: Scene | THREE.Object3D) { 117 | if (target instanceof Scene) { 118 | this.removeScene(target) 119 | return 120 | } 121 | 122 | this._2dCount -= this.count2dObjects(target) 123 | this.unparent2dObjects(target) 124 | this.scene.remove(target) 125 | } 126 | 127 | /** 128 | * Removes all rendered objects 129 | */ 130 | clear () { 131 | this.scene.clear() 132 | this._boundingBox = undefined 133 | this._memory = 0 134 | } 135 | 136 | private addScene (scene: Scene) { 137 | this._vimScenes.push(scene) 138 | scene.meshes.forEach((m) => { 139 | this.scene.add(m.mesh) 140 | }) 141 | 142 | this.updateBox(scene.getBoundingBox()) 143 | 144 | // Memory 145 | this._memory += scene.getMemory() 146 | } 147 | 148 | updateBox (box: THREE.Box3 | undefined) { 149 | if (!box) return 150 | this.boxUpdated = true 151 | this._boundingBox = this._boundingBox ? this._boundingBox.union(box) : box 152 | } 153 | 154 | private removeScene (scene: Scene) { 155 | // Remove from array 156 | this._vimScenes = this._vimScenes.filter((f) => f !== scene) 157 | 158 | // Remove all meshes from three scene 159 | for (let i = 0; i < scene.meshes.length; i++) { 160 | this.scene.remove(scene.meshes[i].mesh) 161 | } 162 | 163 | // Recompute bounding box 164 | this._boundingBox = 165 | this._vimScenes.length > 0 166 | ? this._vimScenes 167 | .map((s) => s.getBoundingBox()) 168 | .reduce((b1, b2) => b1.union(b2)) 169 | : undefined 170 | this._memory -= scene.getMemory() 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/rendering/renderingSection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/rendering 3 | */ 4 | 5 | import * as THREE from 'three' 6 | import { ViewerMaterials } from '../../vim-loader/materials/viewerMaterials' 7 | import { Renderer } from './renderer' 8 | 9 | /** 10 | * Manages a section box from renderer clipping planes 11 | */ 12 | export class RenderingSection { 13 | private _renderer: Renderer 14 | 15 | private _materials: ViewerMaterials 16 | private _active: boolean = true 17 | 18 | /** 19 | * Current section box. To update the box use fitbox. 20 | */ 21 | readonly box: THREE.Box3 = new THREE.Box3( 22 | new THREE.Vector3(-100, -100, -100), 23 | new THREE.Vector3(100, 100, 100) 24 | ) 25 | 26 | private maxX: THREE.Plane = new THREE.Plane(new THREE.Vector3(-1, 0, 0)) 27 | private minX: THREE.Plane = new THREE.Plane(new THREE.Vector3(1, 0, 0)) 28 | private maxY: THREE.Plane = new THREE.Plane(new THREE.Vector3(0, -1, 0)) 29 | private minY: THREE.Plane = new THREE.Plane(new THREE.Vector3(0, 1, 0)) 30 | private maxZ: THREE.Plane = new THREE.Plane(new THREE.Vector3(0, 0, -1)) 31 | private minZ: THREE.Plane = new THREE.Plane(new THREE.Vector3(0, 0, 1)) 32 | private planes: THREE.Plane[] = [ 33 | this.maxX, 34 | this.minX, 35 | this.maxY, 36 | this.minY, 37 | this.maxZ, 38 | this.minZ 39 | ] 40 | 41 | constructor (renderer: Renderer, materials: ViewerMaterials) { 42 | this._renderer = renderer 43 | this._materials = materials 44 | } 45 | 46 | /** 47 | * Resizes the section box to match the dimensions of the provided bounding box. 48 | * @param box The bounding box to match the section box to. 49 | */ 50 | fitBox (box: THREE.Box3) { 51 | this.maxX.constant = box.max.x 52 | this.minX.constant = -box.min.x 53 | this.maxY.constant = box.max.y 54 | this.minY.constant = -box.min.y 55 | this.maxZ.constant = box.max.z 56 | this.minZ.constant = -box.min.z 57 | this.box.copy(box) 58 | this._renderer.needsUpdate = true 59 | this._renderer.skipAntialias = true 60 | } 61 | 62 | /** 63 | * Determines whether objecets outside the section box will be culled or not. 64 | */ 65 | set active (value: boolean) { 66 | this._materials.clippingPlanes = this.planes 67 | this._renderer.renderer.localClippingEnabled = value 68 | this._active = value 69 | this._renderer.needsUpdate = true 70 | } 71 | 72 | get active () { 73 | return this._active 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/rendering/transferPass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module viw-webgl-viewer/rendering 3 | */ 4 | 5 | import THREE from 'three' 6 | import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass' 7 | import { createTransferMaterial } from '../../vim-loader/materials/transferMaterial' 8 | 9 | /** 10 | * Copies a source buffer to the current write buffer. 11 | */ 12 | export class TransferPass extends Pass { 13 | private _fsQuad: FullScreenQuad 14 | private _uniforms: { [uniform: string]: THREE.IUniform } 15 | 16 | constructor (sceneTexture: THREE.Texture) { 17 | super() 18 | 19 | this._fsQuad = new FullScreenQuad() 20 | const mat = createTransferMaterial() 21 | this._fsQuad.material = mat 22 | this._uniforms = mat.uniforms 23 | this._uniforms.source.value = sceneTexture 24 | } 25 | 26 | dispose () { 27 | this._fsQuad.dispose() 28 | } 29 | 30 | render ( 31 | renderer: THREE.WebGLRenderer, 32 | writeBuffer: THREE.WebGLRenderTarget, 33 | readBuffer: THREE.WebGLRenderTarget 34 | ) { 35 | // 2. Draw the outlines using the depth texture and normal texture 36 | // and combine it with the scene color 37 | if (this.renderToScreen) { 38 | // If this is the last effect, then renderToScreen is true. 39 | // So we should render to the screen by setting target null 40 | // Otherwise, just render into the writeBuffer that the next effect will use as its read buffer. 41 | renderer.setRenderTarget(null) 42 | this._fsQuad.render(renderer) 43 | } else { 44 | renderer.setRenderTarget(writeBuffer) 45 | this._fsQuad.render(renderer) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/settings/defaultViewerSettings.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { floor } from '../../images' 3 | import { AxesSettings } from '../gizmos/axes/axesSettings' 4 | import { ViewerSettings } from './viewerSettings' 5 | 6 | /** 7 | * Defines the default values for the VIM Viewer settings. 8 | */ 9 | export const defaultViewerSettings: ViewerSettings = { 10 | canvas: { 11 | id: undefined, 12 | resizeDelay: 200 13 | }, 14 | camera: { 15 | orthographic: false, 16 | allowedMovement: new THREE.Vector3(1, 1, 1), 17 | allowedRotation: new THREE.Vector2(1, 1), 18 | near: 0.001, 19 | far: 15000, 20 | fov: 50, 21 | zoom: 1, 22 | // 45 deg down looking down z. 23 | forward: new THREE.Vector3(1, -1, 1), 24 | controls: { 25 | orbit: true, 26 | rotateSpeed: 1, 27 | orbitSpeed: 1, 28 | moveSpeed: 1, 29 | scrollSpeed: 1.5 30 | }, 31 | 32 | gizmo: { 33 | enable: true, 34 | size: 0.01, 35 | color: new THREE.Color(0x444444), 36 | opacity: 0.3, 37 | opacityAlways: 0.02 38 | } 39 | }, 40 | background: { color: new THREE.Color(0xc1c2c6) }, 41 | skybox: { 42 | enable: true, 43 | skyColor: new THREE.Color(0xe6f4fa), // Light sky blue pastel 44 | groundColor: new THREE.Color(0xdfdfe1), // Light earthy brown pastel 45 | sharpness: 2 46 | }, 47 | groundPlane: { 48 | visible: false, 49 | encoding: 'base64', 50 | texture: floor, 51 | opacity: 1, 52 | color: new THREE.Color(0xffffff), 53 | size: 5 54 | }, 55 | skylight: { 56 | skyColor: new THREE.Color(0xffffff), 57 | groundColor: new THREE.Color(0xffffff), 58 | intensity: 0.8 59 | }, 60 | sunlights: [ 61 | { 62 | followCamera: true, 63 | position: new THREE.Vector3(1000, 1000, 1000), 64 | color: new THREE.Color(0xffffff), 65 | intensity: 0.8 66 | }, 67 | { 68 | followCamera: true, 69 | position: new THREE.Vector3(-1000, -1000, -1000), 70 | color: new THREE.Color(0xffffff), 71 | intensity: 0.2 72 | } 73 | ], 74 | materials: { 75 | standard: { 76 | color: new THREE.Color(0x999999) 77 | }, 78 | highlight: { 79 | color: new THREE.Color(0x6ad2ff), 80 | opacity: 0.5 81 | }, 82 | isolation: { 83 | color: new THREE.Color(0x4E525C), 84 | opacity: 0.08 85 | }, 86 | section: { 87 | strokeWidth: 0.01, 88 | strokeFalloff: 0.75, 89 | strokeColor: new THREE.Color(0xf6f6f6) 90 | }, 91 | outline: { 92 | intensity: 3, 93 | falloff: 3, 94 | blur: 2, 95 | color: new THREE.Color(0x00ffff) 96 | } 97 | }, 98 | axes: new AxesSettings(), 99 | rendering: { 100 | onDemand: true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/vim-webgl-viewer/viewport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | @module viw-webgl-viewer 3 | */ 4 | 5 | import { SignalDispatcher } from 'ste-signals' 6 | import * as THREE from 'three' 7 | import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' 8 | import { ViewerSettings } from './settings/viewerSettings' 9 | 10 | export class Viewport { 11 | /** 12 | * HTML Canvas on which the model is rendered 13 | */ 14 | readonly canvas: HTMLCanvasElement 15 | /** HTML Element in which text is rendered */ 16 | readonly textRenderer : CSS2DRenderer 17 | 18 | get text () { 19 | return this.textRenderer.domElement 20 | } 21 | 22 | private _unregisterResize: Function | undefined 23 | private _ownedCanvas: boolean 24 | private _onResize: SignalDispatcher = new SignalDispatcher() 25 | private _onReparent: SignalDispatcher = new SignalDispatcher() 26 | 27 | /** 28 | * Signal dispatched when the canvas reparented. 29 | */ 30 | get onReparent () { 31 | return this._onReparent.asEvent() 32 | } 33 | 34 | /** 35 | * Signal dispatched when the canvas is resized. 36 | */ 37 | get onResize () { 38 | return this._onResize.asEvent() 39 | } 40 | 41 | /** 42 | * Constructs a new instance of the class with the provided settings. 43 | * @param {ViewerSettings} settings The settings object defining viewer configurations. 44 | */ 45 | constructor (settings: ViewerSettings) { 46 | const { canvas, owned } = Viewport.getOrCreateCanvas(settings.canvas.id) 47 | this.canvas = canvas 48 | this.textRenderer = this.createTextRenderer() 49 | this._ownedCanvas = owned 50 | this.watchResize(settings.canvas.resizeDelay) 51 | } 52 | 53 | /** 54 | * Either returns html canvas at provided Id or creates a canvas at root level 55 | */ 56 | private static getOrCreateCanvas (canvasId?: string) { 57 | const canvas = canvasId 58 | ? (document.getElementById(canvasId) as HTMLCanvasElement) 59 | : undefined 60 | 61 | return canvas 62 | ? { canvas, owned: false } 63 | : { canvas: this.createCanvas(), owned: true } 64 | } 65 | 66 | private static createCanvas () { 67 | const canvas = document.createElement('canvas') 68 | canvas.className = 'vim-canvas' 69 | canvas.tabIndex = 0 70 | canvas.style.backgroundColor = 'black' 71 | document.body.appendChild(canvas) 72 | return canvas 73 | } 74 | 75 | /** Returns a text renderer that will render html in an html element sibbling to canvas */ 76 | private createTextRenderer () { 77 | if (!this.canvas.parentElement) { 78 | throw new Error('Cannot create text renderer without a canvas') 79 | } 80 | 81 | const size = this.getParentSize() 82 | const renderer = new CSS2DRenderer() 83 | renderer.setSize(size.x, size.y) 84 | const text = renderer.domElement 85 | 86 | text.className = 'vim-text-renderer' 87 | text.style.position = 'absolute' 88 | text.style.top = '0px' 89 | text.style.pointerEvents = 'none' 90 | this.canvas.parentElement.append(text) 91 | return renderer 92 | } 93 | 94 | get parent () { 95 | return this.canvas.parentElement 96 | } 97 | 98 | reparent (parent: HTMLElement) { 99 | if (this.parent === parent) return 100 | parent.appendChild(this.canvas) 101 | parent.appendChild(this.text) 102 | this._onReparent.dispatch() 103 | } 104 | 105 | /** 106 | * Removes the canvas if it's owned by the viewer. 107 | */ 108 | dispose () { 109 | this._unregisterResize?.() 110 | this._unregisterResize = undefined 111 | 112 | if (this._ownedCanvas) this.canvas.remove() 113 | } 114 | 115 | /** 116 | * Returns the pixel size of the parent element. 117 | * @returns {THREE.Vector2} The pixel size of the parent element. 118 | */ 119 | getParentSize () { 120 | return new THREE.Vector2( 121 | this.getParentWidth(), 122 | this.getParentHeight() 123 | ) 124 | } 125 | 126 | private getParentWidth () { 127 | return this.canvas.parentElement?.clientWidth ?? this.canvas.clientWidth 128 | } 129 | 130 | private getParentHeight () { 131 | return this.canvas.parentElement?.clientHeight ?? this.canvas.clientHeight 132 | } 133 | 134 | /** 135 | * Returns the pixel size of the canvas. 136 | * @returns {THREE.Vector2} The pixel size of the canvas. 137 | */ 138 | getSize () { 139 | return new THREE.Vector2(this.canvas.clientWidth, this.canvas.clientHeight) 140 | } 141 | 142 | /** 143 | * Calculates and returns the aspect ratio of the parent element. 144 | * @returns {number} The aspect ratio (width divided by height) of the parent element. 145 | */ 146 | getAspectRatio () { 147 | return this.getParentWidth() / this.getParentHeight() 148 | } 149 | 150 | /** 151 | * Resizes the canvas and updates the camera to match new parent dimensions. 152 | */ 153 | ResizeToParent () { 154 | this._onResize.dispatch() 155 | } 156 | 157 | /** 158 | * Set a callback for canvas resize with debouncing 159 | * https://stackoverflow.com/questions/5825447/javascript-event-for-canvas-resize/30688151 160 | * @param callback code to be called 161 | * @param timeout time after the last resize before code will be called 162 | */ 163 | private watchResize (timeout: number) { 164 | let timerId: ReturnType | undefined 165 | const onResize = () => { 166 | if (timerId !== undefined) { 167 | clearTimeout(timerId) 168 | timerId = undefined 169 | } 170 | timerId = setTimeout(() => { 171 | timerId = undefined 172 | this._onResize.dispatch() 173 | }, timeout) 174 | } 175 | window.addEventListener('resize', onResize) 176 | 177 | this._unregisterResize = () => 178 | window.removeEventListener('resize', onResize) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": false, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "lib": ["esnext", "dom"], 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "files": ["src/index.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | 4 | export default defineConfig({ 5 | build: { 6 | sourcemap: true, 7 | lib: { 8 | formats: ['iife', 'es'], 9 | entry: resolve(__dirname, './src/index.ts'), 10 | name: 'VIM' 11 | }, 12 | 13 | // Minify set to true will break the IIFE output 14 | minify: false, 15 | } 16 | }) 17 | --------------------------------------------------------------------------------