├── .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 |
--------------------------------------------------------------------------------