| - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | ||||
| - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | ||||
| - [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets | - [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets | ||||
| - [TransformControlsPlugin](#transformcontrolsplugin) - Adds support for moving, rotating and scaling objects in the viewer with interactive widgets | |||||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | ||||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | ||||
| - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views | - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views | ||||
| - [FragmentClippingExtensionPlugin](#fragmentclippingextensionplugin) - Fragment/SDF Clipping material extension for PhysicalMaterial | - [FragmentClippingExtensionPlugin](#fragmentclippingextensionplugin) - Fragment/SDF Clipping material extension for PhysicalMaterial | ||||
| - [HDRiGroundPlugin](#hdrigroundplugin) - Add support for ground projected hdri/skybox to the webgl background shader. | - [HDRiGroundPlugin](#hdrigroundplugin) - Add support for ground projected hdri/skybox to the webgl background shader. | ||||
| - [VirtualCamerasPlugin](#virtualcamerasplugin) - Add support for rendering virtual cameras before the main one every frame. | - [VirtualCamerasPlugin](#virtualcamerasplugin) - Add support for rendering virtual cameras before the main one every frame. | ||||
| - [EditorViewWidgetPlugin](#editorviewwidgetplugin) - Adds an interactive ViewHelper/AxisHelper that syncs with the main camera. | |||||
| - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | ||||
| - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | ||||
| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | ||||
| ``` | ``` | ||||
| ## TransformControlsPlugin | |||||
| [//]: # (todo: image) | |||||
| [Example](https://threepipe.org/examples/#transform-controls-plugin/) — | |||||
| [Source Code](./src/plugins/interaction/TransformControlsPlugin.ts) — | |||||
| [API Reference](https://threepipe.org/docs/classes/TransformControlsPlugin.html) | |||||
| Transform Controls Plugin adds support for moving, rotating and scaling objects in the viewer with interactive widgets. | |||||
| Under the hood, TransformControlsPlugin uses [TransformControls2](https://threepipe.org/docs/classes/TransformControls2) to provide the interactive controls, it is a extended version of three.js [TransformControls](https://threejs.org/docs/#examples/en/controls/TransformControls). | |||||
| When the plugin is added to the viewer, it interfaces with the [PickingPlugin](#pickingplugin) and shows the control gizmos when an object is selected and hides them when the object is unselected. | |||||
| If the PickingPlugin is not added to the viewer before the TransformControlsPlugin, it is added automatically with the plugin. | |||||
| ```typescript | |||||
| import {ThreeViewer, TransformControlsPlugin} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const transfromControlsPlugin = viewer.addPluginSync(new TransformControlsPlugin()) | |||||
| // Get the underlying transform controls | |||||
| console.log(transfromControlsPlugin.transformControls) | |||||
| ``` | |||||
| ## GLTFAnimationPlugin | ## GLTFAnimationPlugin | ||||
| [//]: # (todo: image) | [//]: # (todo: image) | ||||
| const viewer = new ThreeViewer({...}) | const viewer = new ThreeViewer({...}) | ||||
| const hdriGround = viewer.addPluginSync(new VirtualCamerasPlugin()) | |||||
| const virtualCameras = viewer.addPluginSync(new VirtualCamerasPlugin()) | |||||
| const camera = new PerspectiveCamera2('orbit', viewer.canvas, false, 45, 1) | const camera = new PerspectiveCamera2('orbit', viewer.canvas, false, 45, 1) | ||||
| camera.name = name | camera.name = name | ||||
| console.log(vCam.target) // target is a WebGLRenderTarget/IRenderTarget | console.log(vCam.target) // target is a WebGLRenderTarget/IRenderTarget | ||||
| ``` | ``` | ||||
| Check the [virtual camera](https://threepipe.org/examples/#hdri-ground-plugin/) example for using the texture in the scene. | |||||
| Check the [virtual camera](https://threepipe.org/examples/#virtual-camera/) example for using the texture in the scene. | |||||
| ## EditorViewWidgetPlugin | |||||
| [//]: # (todo: image) | |||||
| [Example](https://threepipe.org/examples/#editor-view-widget-plugin/) — | |||||
| [Source Code](./src/plugins/interaction/EditorViewWidgetPlugin.ts) — | |||||
| [API Reference](https://threepipe.org/docs/classes/EditorViewWidgetPlugin.html) | |||||
| EditorViewWidgetPlugin adds a ViewHelper in the parent of the viewer canvas to show the current camera view and allow the user to change the camera view to one of the primary world axes. | |||||
| Simply add the plugin to the viewer to see the widget. | |||||
| ```typescript | |||||
| import {ThreeViewer, EditorViewWidgetPlugin} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const plugin = viewer.addPluginSync(new EditorViewWidgetPlugin()) | |||||
| // to hide the widget | |||||
| plugin.enabled = false | |||||
| ``` | |||||
| ## Rhino3dmLoadPlugin | ## Rhino3dmLoadPlugin | ||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Editor View Widget Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, EditorViewWidgetPlugin, IObject3D, ThreeViewer, timeout} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| }) | |||||
| viewer.scene.setBackgroundColor(0x151822) | |||||
| const plugin = viewer.addPluginSync(new EditorViewWidgetPlugin('bottom-left', 256)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf') | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| ui.setupPluginUi(EditorViewWidgetPlugin) | |||||
| // look at the model from left | |||||
| plugin.setOrientation('-z') | |||||
| await timeout(1000) // wait for 1 sec | |||||
| // look at the model from back | |||||
| plugin.setOrientation('-x') | |||||
| await timeout(1000) // wait for 1 sec | |||||
| // look at the model from front | |||||
| plugin.setOrientation('+z') | |||||
| } | |||||
| init().then(_testFinish) |
| <li><a href="./picking-plugin/">Picking (Selection) Plugin </a></li> | <li><a href="./picking-plugin/">Picking (Selection) Plugin </a></li> | ||||
| <li><a href="./camera-view-plugin/">Camera View (Animation) Plugin </a></li> | <li><a href="./camera-view-plugin/">Camera View (Animation) Plugin </a></li> | ||||
| <li><a href="./dropzone-plugin/">Dropzone (Drag & Drop) Plugin </a></li> | <li><a href="./dropzone-plugin/">Dropzone (Drag & Drop) Plugin </a></li> | ||||
| <li><a href="./transform-controls-plugin/">Transform Controls Plugin </a></li> | |||||
| <li><a href="./editor-view-widget-plugin/">Editor View Widget Plugin </a></li> | |||||
| <li><a href="./fullscreen-plugin/">FullScreen Plugin </a></li> | <li><a href="./fullscreen-plugin/">FullScreen Plugin </a></li> | ||||
| <li><a href="./geometry-generator-plugin/">Geometry Generator Plugin </a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Import</h2> | <h2 class="category">Import</h2> | ||||
| <ul> | <ul> | ||||
| <ul> | <ul> | ||||
| <li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li> | <li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li> | ||||
| <li><a href="./render-target-preview/">Render Target Preview Plugin </a></li> | <li><a href="./render-target-preview/">Render Target Preview Plugin </a></li> | ||||
| <li><a href="./geometry-generator-plugin/">Geometry Generator Plugin </a></li> | |||||
| <li><a href="./geometry-uv-preview/">Geometry UV Preview Plugin </a></li> | <li><a href="./geometry-uv-preview/">Geometry UV Preview Plugin </a></li> | ||||
| <li><a href="./parallel-asset-import/">Parallel Asset Import </a></li> | <li><a href="./parallel-asset-import/">Parallel Asset Import </a></li> | ||||
| <li><a href="./obj-to-glb/">Convert OBJ to GLB </a></li> | <li><a href="./obj-to-glb/">Convert OBJ to GLB </a></li> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Transform Controls Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, IObject3D, PickingPlugin, ThreeViewer, TransformControlsPlugin} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| }) | |||||
| viewer.scene.setBackgroundColor(0x151822) | |||||
| const picking = viewer.addPluginSync(PickingPlugin) | |||||
| const transformControlsPlugin = viewer.addPluginSync(TransformControlsPlugin) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| const model = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf') | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| ui.setupPluginUi(TransformControlsPlugin) | |||||
| ui.setupPluginUi(PickingPlugin) | |||||
| // Get the underlying transform controls (instance of TransformControls2) | |||||
| const transformControls = transformControlsPlugin.transformControls | |||||
| console.log(transformControls) | |||||
| // Transform controls plugin automatically tracks the selected object in the PickingPlugin and shows the transform controls | |||||
| picking.setSelectedObject(model) | |||||
| } | |||||
| init().then(_testFinish) |
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | ||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | ||||
| </head> | </head> | ||||
| <body> | |||||
| <body class="code-preview-container"> | |||||
| <div id="canvas-container"> | <div id="canvas-container"> | ||||
| <canvas id="mcanvas"></canvas> | <canvas id="mcanvas"></canvas> | ||||
| </div> | </div> |
| CustomBumpMapPlugin, | CustomBumpMapPlugin, | ||||
| DepthBufferPlugin, | DepthBufferPlugin, | ||||
| DropzonePlugin, | DropzonePlugin, | ||||
| EditorViewWidgetPlugin, | |||||
| FilmicGrainPlugin, | FilmicGrainPlugin, | ||||
| FragmentClippingExtensionPlugin, | FragmentClippingExtensionPlugin, | ||||
| FrameFadePlugin, | FrameFadePlugin, | ||||
| STLLoadPlugin, | STLLoadPlugin, | ||||
| ThreeViewer, | ThreeViewer, | ||||
| TonemapPlugin, | TonemapPlugin, | ||||
| TransformControlsPlugin, | |||||
| USDZLoadPlugin, | USDZLoadPlugin, | ||||
| ViewerUiConfigPlugin, | ViewerUiConfigPlugin, | ||||
| VignettePlugin, | VignettePlugin, | ||||
| new ProgressivePlugin(), | new ProgressivePlugin(), | ||||
| GLTFAnimationPlugin, | GLTFAnimationPlugin, | ||||
| PickingPlugin, | PickingPlugin, | ||||
| TransformControlsPlugin, | |||||
| EditorViewWidgetPlugin, | |||||
| CameraViewPlugin, | CameraViewPlugin, | ||||
| ViewerUiConfigPlugin, | ViewerUiConfigPlugin, | ||||
| ClearcoatTintPlugin, | ClearcoatTintPlugin, | ||||
| editor.loadPlugins({ | editor.loadPlugins({ | ||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ||||
| ['Interaction']: [HierarchyUiPlugin, PickingPlugin, GeometryGeneratorPlugin], | |||||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin], | |||||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin], | |||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin], | |||||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | ||||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], | ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], | ||||
| ['Debug']: [RenderTargetPreviewPlugin], | ['Debug']: [RenderTargetPreviewPlugin], | ||||
| }) | }) | ||||
| viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 5)) | |||||
| const hemiLight = viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 5)) | |||||
| hemiLight.name = 'Hemisphere Light' | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | ||||
| // const result = await viewer.load<IObject3D>('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', { | // const result = await viewer.load<IObject3D>('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', { |
| { | { | ||||
| "name": "threepipe", | "name": "threepipe", | ||||
| "version": "0.0.18", | |||||
| "version": "0.0.19", | |||||
| "lockfileVersion": 2, | "lockfileVersion": 2, | ||||
| "requires": true, | "requires": true, | ||||
| "packages": { | "packages": { | ||||
| "": { | "": { | ||||
| "name": "threepipe", | "name": "threepipe", | ||||
| "version": "0.0.18", | |||||
| "version": "0.0.19", | |||||
| "license": "Apache-2.0", | "license": "Apache-2.0", | ||||
| "dependencies": { | "dependencies": { | ||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1018/package.tgz", | "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1018/package.tgz", | ||||
| "@types/webxr": "^0.5.1", | "@types/webxr": "^0.5.1", | ||||
| "@types/wicg-file-system-access": "^2020.9.5", | "@types/wicg-file-system-access": "^2020.9.5", | ||||
| "ts-browser-helpers": "^0.8.0" | |||||
| "stats.js": "^0.17.0", | |||||
| "ts-browser-helpers": "^0.9.0", | |||||
| "uiconfig.js": "^0.0.9" | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@rollup/plugin-commonjs": "^25.0.0", | "@rollup/plugin-commonjs": "^25.0.0", | ||||
| "rollup-plugin-glsl": "^1.3.0", | "rollup-plugin-glsl": "^1.3.0", | ||||
| "rollup-plugin-license": "^3.0.1", | "rollup-plugin-license": "^3.0.1", | ||||
| "rollup-plugin-postcss": "^4.0.2", | "rollup-plugin-postcss": "^4.0.2", | ||||
| "stats.js": "^0.17.0", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2018/package.tgz", | "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2018/package.tgz", | ||||
| "tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
| "typedoc": "^0.24.7", | "typedoc": "^0.24.7", | ||||
| "typescript": "^5.0.4", | "typescript": "^5.0.4", | ||||
| "typescript-plugin-css-modules": "^5.0.1", | |||||
| "uiconfig.js": "^0.0.9" | |||||
| "typescript-plugin-css-modules": "^5.0.1" | |||||
| }, | }, | ||||
| "optionalDependencies": { | "optionalDependencies": { | ||||
| "win-node-env": "^0.6.1" | "win-node-env": "^0.6.1" | ||||
| "node_modules/stats.js": { | "node_modules/stats.js": { | ||||
| "version": "0.17.0", | "version": "0.17.0", | ||||
| "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", | "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", | ||||
| "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", | |||||
| "dev": true | |||||
| "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" | |||||
| }, | }, | ||||
| "node_modules/statuses": { | "node_modules/statuses": { | ||||
| "version": "1.5.0", | "version": "1.5.0", | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/ts-browser-helpers": { | "node_modules/ts-browser-helpers": { | ||||
| "version": "0.8.0", | |||||
| "resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.8.0.tgz", | |||||
| "integrity": "sha512-r7k6udt+tS/FZLwTUbzSgHdHQ56R/MD1oS2rFcEk8O4jKUTzuDVqlUoLX1EjJQYEBGMBI8TEyySTkuomxewZEw==", | |||||
| "version": "0.9.0", | |||||
| "resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.9.0.tgz", | |||||
| "integrity": "sha512-8ViPxn9X/K+is+4i8x9H1MB0RmPYFqkrrX2v9EtVZxGbYXLaKmkiL+kfzISUGNOQdzu4E93KjluT9S7cxegs0g==", | |||||
| "dependencies": { | "dependencies": { | ||||
| "@types/wicg-file-system-access": "^2020.9.5" | "@types/wicg-file-system-access": "^2020.9.5" | ||||
| } | } | ||||
| "node_modules/uiconfig.js": { | "node_modules/uiconfig.js": { | ||||
| "version": "0.0.9", | "version": "0.0.9", | ||||
| "resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.9.tgz", | "resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.9.tgz", | ||||
| "integrity": "sha512-jB2LIUTBA7gHegrFr62J7xOEkyXmb+h0/5FczD9qzcBjCbb2R8UsdaxOyVoa+9ecfc2NAaeboTzJK8n8ji1cbw==", | |||||
| "dev": true | |||||
| "integrity": "sha512-jB2LIUTBA7gHegrFr62J7xOEkyXmb+h0/5FczD9qzcBjCbb2R8UsdaxOyVoa+9ecfc2NAaeboTzJK8n8ji1cbw==" | |||||
| }, | }, | ||||
| "node_modules/unbox-primitive": { | "node_modules/unbox-primitive": { | ||||
| "version": "1.0.2", | "version": "1.0.2", | ||||
| "stats.js": { | "stats.js": { | ||||
| "version": "0.17.0", | "version": "0.17.0", | ||||
| "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", | "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", | ||||
| "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", | |||||
| "dev": true | |||||
| "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" | |||||
| }, | }, | ||||
| "statuses": { | "statuses": { | ||||
| "version": "1.5.0", | "version": "1.5.0", | ||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "ts-browser-helpers": { | "ts-browser-helpers": { | ||||
| "version": "0.8.0", | |||||
| "resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.8.0.tgz", | |||||
| "integrity": "sha512-r7k6udt+tS/FZLwTUbzSgHdHQ56R/MD1oS2rFcEk8O4jKUTzuDVqlUoLX1EjJQYEBGMBI8TEyySTkuomxewZEw==", | |||||
| "version": "0.9.0", | |||||
| "resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.9.0.tgz", | |||||
| "integrity": "sha512-8ViPxn9X/K+is+4i8x9H1MB0RmPYFqkrrX2v9EtVZxGbYXLaKmkiL+kfzISUGNOQdzu4E93KjluT9S7cxegs0g==", | |||||
| "requires": { | "requires": { | ||||
| "@types/wicg-file-system-access": "^2020.9.5" | "@types/wicg-file-system-access": "^2020.9.5" | ||||
| } | } | ||||
| "uiconfig.js": { | "uiconfig.js": { | ||||
| "version": "0.0.9", | "version": "0.0.9", | ||||
| "resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.9.tgz", | "resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.9.tgz", | ||||
| "integrity": "sha512-jB2LIUTBA7gHegrFr62J7xOEkyXmb+h0/5FczD9qzcBjCbb2R8UsdaxOyVoa+9ecfc2NAaeboTzJK8n8ji1cbw==", | |||||
| "dev": true | |||||
| "integrity": "sha512-jB2LIUTBA7gHegrFr62J7xOEkyXmb+h0/5FczD9qzcBjCbb2R8UsdaxOyVoa+9ecfc2NAaeboTzJK8n8ji1cbw==" | |||||
| }, | }, | ||||
| "unbox-primitive": { | "unbox-primitive": { | ||||
| "version": "1.0.2", | "version": "1.0.2", |
| export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin' | export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin' | ||||
| export {FullScreenPlugin} from './interaction/FullScreenPlugin' | export {FullScreenPlugin} from './interaction/FullScreenPlugin' | ||||
| export {PickingPlugin} from './interaction/PickingPlugin' | export {PickingPlugin} from './interaction/PickingPlugin' | ||||
| export {TransformControlsPlugin} from './interaction/TransformControlsPlugin' | |||||
| export {EditorViewWidgetPlugin} from './interaction/EditorViewWidgetPlugin' | |||||
| // import | // import | ||||
| export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' | export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' |
| import {AViewerPluginSync, type IViewerEvent, ThreeViewer} from '../../viewer' | |||||
| import {DomPlacement, GizmoOrientation, ViewHelper2} from '../../three' | |||||
| import {uiFolderContainer, uiToggle} from 'uiconfig.js' | |||||
| import {onChange} from 'ts-browser-helpers' | |||||
| @uiFolderContainer('Editor View Widget') | |||||
| export class EditorViewWidgetPlugin extends AViewerPluginSync<''> { | |||||
| public static readonly PluginType = 'EditorViewWidgetPlugin' | |||||
| @uiToggle() | |||||
| @onChange(EditorViewWidgetPlugin.prototype._enableChange) | |||||
| enabled = true | |||||
| protected _enableChange() { | |||||
| if (!this._viewer || !this.widget) return | |||||
| this.widget.domContainer.style.display = this.enabled ? 'block' : 'none' | |||||
| } | |||||
| constructor(public readonly placement: DomPlacement = 'top-left', public readonly size = 128) { | |||||
| super() | |||||
| } | |||||
| widget?: ViewHelper2 | |||||
| onAdded(v: ThreeViewer) { | |||||
| super.onAdded(v) | |||||
| this.widget = new ViewHelper2(v.scene.mainCamera as any, v.canvas, this.placement, this.size) | |||||
| this.widget.target = v.scene.mainCamera.target | |||||
| this.widget.addEventListener('animating-changed', (e)=>{ | |||||
| const val = e.detail.value | |||||
| v.scene.mainCamera.setInteractions(!val, EditorViewWidgetPlugin.PluginType) | |||||
| }) | |||||
| this.widget.addEventListener('update', ()=>this._needsRender = true) // when mouse hover and leave. | |||||
| v.scene.addEventListener('mainCameraChange', this._mainCameraChange) | |||||
| v.scene.addEventListener('mainCameraUpdate', this._mainCameraUpdate) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| this.widget?.dispose() | |||||
| this.widget = undefined | |||||
| viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange) | |||||
| viewer.scene.removeEventListener('mainCameraUpdate', this._mainCameraUpdate) | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| protected _mainCameraChange() { | |||||
| if (!this._viewer || !this.widget) return | |||||
| this.widget.camera = this._viewer.scene.mainCamera as any | |||||
| } | |||||
| protected _mainCameraUpdate() { | |||||
| if (!this._viewer || !this.widget) return | |||||
| this.widget.target = this._viewer.scene.mainCamera.target | |||||
| } | |||||
| // this is required separately so that when we hover on the gizmo we dont need to set dirty for the whole viewer | |||||
| protected _needsRender = false | |||||
| protected _viewerListeners = { | |||||
| postRender: (_: IViewerEvent)=>{ | |||||
| if (!this._viewer || !this.widget || !this.enabled) return | |||||
| this._needsRender = true | |||||
| }, | |||||
| postFrame: (_: IViewerEvent)=>{ | |||||
| if (!this._viewer || !this.widget || !this.enabled || !this._needsRender) return | |||||
| this.widget.update() | |||||
| this.widget.render() | |||||
| if (this.widget.animating) this._viewer.scene.mainCamera.setDirty() | |||||
| this._needsRender = false | |||||
| }, | |||||
| } | |||||
| setOrientation(orientation: GizmoOrientation) { | |||||
| if (!this.widget) return | |||||
| this.widget.setOrientation(orientation) | |||||
| } | |||||
| } | |||||
| this._widget?.attach(this._picker.selectedObject) | this._widget?.attach(this._picker.selectedObject) | ||||
| else | else | ||||
| this._widget?.detach() | this._widget?.detach() | ||||
| this.uiConfig?.uiRefresh?.(true) | |||||
| } | } | ||||
| public setDirty() { | public setDirty() { |
| import {uiConfig, uiPanelContainer, uiToggle} from 'uiconfig.js' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {OrbitControls3, TransformControls2} from '../../three' | |||||
| import {PickingPlugin} from './PickingPlugin' | |||||
| import {onChange} from 'ts-browser-helpers' | |||||
| @uiPanelContainer('Transform Controls') | |||||
| export class TransformControlsPlugin extends AViewerPluginSync<''> { | |||||
| public static readonly PluginType = 'TransformControlsPlugin' | |||||
| @uiToggle() | |||||
| @onChange(TransformControlsPlugin.prototype._enableChange) | |||||
| enabled = true | |||||
| private _pickingWidgetDisabled = false | |||||
| private _enableChange() { | |||||
| if (!this._viewer) return | |||||
| const picking = this._viewer.getPlugin(PickingPlugin)! | |||||
| if (this.enabled && picking.widgetEnabled) { | |||||
| picking.widgetEnabled = false | |||||
| this._pickingWidgetDisabled = true | |||||
| } else if (!this.enabled && this._pickingWidgetDisabled) { | |||||
| picking.widgetEnabled = true | |||||
| this._pickingWidgetDisabled = false | |||||
| } | |||||
| if (this.transformControls) { | |||||
| if (this.enabled && picking.getSelectedObject()) this.transformControls.attach(picking.getSelectedObject()!) | |||||
| else this.transformControls.detach() | |||||
| } | |||||
| } | |||||
| toJSON: any = undefined | |||||
| dependencies = [PickingPlugin] | |||||
| @uiConfig() | |||||
| transformControls: TransformControls2 | undefined | |||||
| protected _isInteracting = false | |||||
| protected _viewerListeners = { | |||||
| postFrame: ()=>{ | |||||
| if (!this.transformControls || !this._viewer) return | |||||
| // this._viewer.scene.mainCamera.setInteractions(!this._isInteracting, TransformControlsPlugin.PluginType) | |||||
| }, | |||||
| } | |||||
| onAdded(viewer: ThreeViewer) { | |||||
| super.onAdded(viewer) | |||||
| this._enableChange() | |||||
| this.transformControls = new TransformControls2(viewer.scene.mainCamera, viewer.canvas) | |||||
| this._mainCameraChange = this._mainCameraChange.bind(this) | |||||
| viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange) | |||||
| this.transformControls.addEventListener('dragging-changed', (event) => { | |||||
| if (!this?._viewer) return | |||||
| const controls = this._viewer.scene.mainCamera.controls | |||||
| if (typeof (controls as any)?.stopDamping === 'function' && controls?.enabled) (controls as OrbitControls3).stopDamping() | |||||
| this._viewer.scene.mainCamera.setInteractions(!event.value, TransformControlsPlugin.PluginType) | |||||
| // this._viewer.scene.mainCamera.autoNearFar = !event.value // todo: maintain state | |||||
| }) | |||||
| this.transformControls.addEventListener('axis-changed', (event) => { | |||||
| if (!this?._viewer) return | |||||
| this._isInteracting = !!event.value | |||||
| const controls = this._viewer.scene.mainCamera.controls | |||||
| if (typeof (controls as any)?.stopDamping === 'function' && controls?.enabled) (controls as OrbitControls3).stopDamping() | |||||
| this._viewer.setDirty() // rerender for color change | |||||
| }) | |||||
| viewer.scene.addObject(this.transformControls, {addToRoot: true}) | |||||
| const picking = viewer.getPlugin(PickingPlugin)! | |||||
| picking.addEventListener('selectedObjectChanged', (event) => { | |||||
| if (!this.transformControls) return | |||||
| if (!this.enabled) { | |||||
| if (this.transformControls.object) this.transformControls.detach() | |||||
| return | |||||
| } | |||||
| event.object ? this.transformControls.attach(event.object) : this.transformControls.detach() | |||||
| }) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange) | |||||
| if (this.transformControls) { | |||||
| this.transformControls.detach() | |||||
| viewer.scene.remove(this.transformControls) | |||||
| this.transformControls.dispose() | |||||
| } | |||||
| this.transformControls = undefined | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| private _mainCameraChange = () => { | |||||
| if (!this.transformControls || !this._viewer) return | |||||
| this.transformControls.camera = this._viewer.scene.mainCamera | |||||
| } | |||||
| } |
| /* eslint-disable */ | |||||
| import {Camera, Mesh, MOUSE, Object3D, Quaternion, Raycaster, Vector3} from 'three'; | |||||
| export class TransformControls extends Object3D { | |||||
| constructor(object: Camera, domElement?: HTMLElement); | |||||
| domElement: HTMLElement; | |||||
| // API | |||||
| camera: Camera; | |||||
| object: Object3D | undefined; | |||||
| enabled: boolean; | |||||
| axis: 'X' | 'Y' | 'Z' | 'E' | 'XY' | 'YZ' | 'XZ' | 'XYZ' | 'XYZE' | null; | |||||
| mode: 'translate' | 'rotate' | 'scale'; | |||||
| translationSnap: number | null; | |||||
| rotationSnap: number | null; | |||||
| space: 'world' | 'local'; | |||||
| size: number; | |||||
| dragging: boolean; | |||||
| showX: boolean; | |||||
| showY: boolean; | |||||
| showZ: boolean; | |||||
| readonly isTransformControls: true; | |||||
| mouseButtons: { LEFT: MOUSE; MIDDLE: MOUSE; RIGHT: MOUSE }; | |||||
| attach(object: Object3D): this; | |||||
| detach(): this; | |||||
| getMode(): 'translate' | 'rotate' | 'scale'; | |||||
| getRaycaster(): Raycaster; | |||||
| setMode(mode: 'translate' | 'rotate' | 'scale'): void; | |||||
| setTranslationSnap(translationSnap: number | null): void; | |||||
| setRotationSnap(rotationSnap: number | null): void; | |||||
| setScaleSnap(scaleSnap: number | null): void; | |||||
| setSize(size: number): void; | |||||
| setSpace(space: 'world' | 'local'): void; | |||||
| reset(): void; | |||||
| dispose(): void; | |||||
| } | |||||
| export class TransformControlsGizmo extends Object3D { | |||||
| type: 'TransformControlsGizmo'; | |||||
| isTransformControlsGizmo: true; | |||||
| gizmo: { | |||||
| translate: Object3D; | |||||
| rotate: Object3D; | |||||
| scale: Object3D; | |||||
| }; | |||||
| helper: { | |||||
| translate: Object3D; | |||||
| rotate: Object3D; | |||||
| scale: Object3D; | |||||
| }; | |||||
| picker: { | |||||
| translate: Object3D; | |||||
| rotate: Object3D; | |||||
| scale: Object3D; | |||||
| }; | |||||
| constructor(); | |||||
| } | |||||
| export class TransformControlsPlane extends Mesh { | |||||
| type: 'TransformControlsPlane'; | |||||
| isTransformControlsPlane: true; | |||||
| constructor(); | |||||
| mode: 'translate' | 'scale' | 'rotate'; | |||||
| axis: 'X' | 'Y' | 'Z' | 'XY' | 'YZ' | 'XZ' | 'XYZ' | 'E'; | |||||
| space: 'local' | 'world'; | |||||
| eye: Vector3; | |||||
| worldPosition: Vector3; | |||||
| worldQuaternion: Quaternion; | |||||
| } |
| import {TransformControls} from './TransformControls.js' | |||||
| import {MathUtils} from 'three' | |||||
| import {ICamera, IObject3D, iObjectCommons, ISceneEvent, IWidget} from '../../core' | |||||
| import {uiDropdown, uiNumber, uiPanelContainer, uiToggle} from 'uiconfig.js' | |||||
| @uiPanelContainer('Transform Controls') | |||||
| export class TransformControls2 extends TransformControls implements IWidget, IObject3D { | |||||
| isWidget = true as const | |||||
| assetType = 'widget' as const | |||||
| setDirty = iObjectCommons.setDirty | |||||
| refreshUi = iObjectCommons.refreshUi.bind(this) | |||||
| object: IObject3D | undefined | |||||
| private _keyDownListener(event: KeyboardEvent) { | |||||
| if (!this.enabled) return | |||||
| if (!this.object) return | |||||
| switch (event.code) { | |||||
| case 'KeyQ': | |||||
| this.space = this.space === 'local' ? 'world' : 'local' | |||||
| break | |||||
| case 'ShiftLeft': | |||||
| this.translationSnap = 0.5 | |||||
| this.rotationSnap = MathUtils.degToRad(15) | |||||
| this.scaleSnap = 0.25 | |||||
| break | |||||
| case 'KeyW': | |||||
| this.mode = 'translate' | |||||
| break | |||||
| case 'KeyE': | |||||
| this.mode = 'rotate' | |||||
| break | |||||
| case 'KeyR': | |||||
| this.mode = 'scale' | |||||
| break | |||||
| case 'Equal': | |||||
| case 'NumpadAdd': | |||||
| case 'Plus': | |||||
| this.size = this.size + 0.1 | |||||
| break | |||||
| case 'Minus': | |||||
| case 'NumpadSubtract': | |||||
| case 'Underscore': | |||||
| this.size = Math.max(this.size - 0.1, 0.1) | |||||
| break | |||||
| case 'KeyX': | |||||
| this.showX = !this.showX | |||||
| break | |||||
| case 'KeyY': | |||||
| this.showY = !this.showY | |||||
| break | |||||
| case 'KeyZ': | |||||
| this.showZ = !this.showZ | |||||
| break | |||||
| case 'Space': | |||||
| this.enabled = !this.enabled | |||||
| break | |||||
| default: | |||||
| return | |||||
| } | |||||
| this.setDirty({refreshScene: true, frameFade: true}) | |||||
| } | |||||
| private _keyUpListener(event: KeyboardEvent) { | |||||
| if (!this.enabled) return | |||||
| // reset events | |||||
| switch (event.code) { | |||||
| case 'ShiftLeft': | |||||
| this.translationSnap = null | |||||
| this.rotationSnap = null | |||||
| this.scaleSnap = null | |||||
| break | |||||
| default: | |||||
| break | |||||
| } | |||||
| if (!this.object) return | |||||
| // non-reset events | |||||
| switch (event.code) { | |||||
| default: | |||||
| break | |||||
| } | |||||
| } | |||||
| constructor(camera: ICamera, canvas: HTMLCanvasElement) { | |||||
| super(camera, canvas) | |||||
| this.visible = false | |||||
| this.userData.bboxVisible = false | |||||
| this.size = 2 | |||||
| this.addEventListener('objectChange', () => { | |||||
| this?.object?.setDirty({fadeFrame: false}) | |||||
| // todo: do this.setDirty? | |||||
| }) | |||||
| this._keyUpListener = this._keyUpListener.bind(this) | |||||
| this._keyDownListener = this._keyDownListener.bind(this) | |||||
| window.addEventListener('keydown', this._keyDownListener) | |||||
| window.addEventListener('keyup', this._keyUpListener) | |||||
| } | |||||
| dispose() { | |||||
| window.removeEventListener('keydown', this._keyDownListener) | |||||
| window.removeEventListener('keyup', this._keyUpListener) | |||||
| super.dispose() | |||||
| } | |||||
| // region properties | |||||
| enabled: boolean | |||||
| // axis: 'X' | 'Y' | 'Z' | 'E' | 'XY' | 'YZ' | 'XZ' | 'XYZ' | 'XYZE' | null | |||||
| @uiDropdown('Mode', ['translate', 'rotate', 'scale'].map(label=>({label}))) | |||||
| mode: 'translate' | 'rotate' | 'scale' | |||||
| translationSnap: number | null | |||||
| rotationSnap: number | null | |||||
| scaleSnap: number | null | |||||
| @uiDropdown('Space', ['world', 'local'].map(label=>({label}))) | |||||
| space: 'world' | 'local' | |||||
| @uiNumber('Size') | |||||
| size: number | |||||
| @uiToggle('Show X') | |||||
| showX: boolean | |||||
| @uiToggle('Show Y') | |||||
| showY: boolean | |||||
| @uiToggle('Show Z') | |||||
| showZ: boolean | |||||
| // dragging: boolean | |||||
| // endregion | |||||
| /** | |||||
| * Get the threejs object | |||||
| * @deprecated | |||||
| */ | |||||
| get modelObject(): this { | |||||
| return this as any | |||||
| } | |||||
| // todo: https://helpx.adobe.com/after-effects/using/3d-transform-gizmo.html | |||||
| // region inherited type fixes | |||||
| traverse: (callback: (object: IObject3D) => void) => void | |||||
| traverseVisible: (callback: (object: IObject3D) => void) => void | |||||
| traverseAncestors: (callback: (object: IObject3D) => void) => void | |||||
| getObjectById: <T extends IObject3D = IObject3D>(id: number) => T | undefined | |||||
| getObjectByName: <T extends IObject3D = IObject3D>(name: string) => T | undefined | |||||
| getObjectByProperty: <T extends IObject3D = IObject3D>(name: string, value: string) => T | undefined | |||||
| copy: (source: this, recursive?: boolean, ...args: any[]) => this | |||||
| clone: (recursive?: boolean) => this | |||||
| remove: (...object: IObject3D[]) => this | |||||
| dispatchEvent: (event: ISceneEvent) => void | |||||
| parent: IObject3D | null | |||||
| children: IObject3D[] | |||||
| // endregion | |||||
| } |
| import { | |||||
| BackSide, | |||||
| Camera, | |||||
| CanvasTexture, | |||||
| Clock, | |||||
| Color, | |||||
| Euler, | |||||
| LinearFilter, | |||||
| Material, | |||||
| Mesh, | |||||
| MeshBasicMaterial, | |||||
| Object3D, | |||||
| OrthographicCamera, | |||||
| PerspectiveCamera, | |||||
| Quaternion, | |||||
| Raycaster, | |||||
| RepeatWrapping, | |||||
| SphereGeometry, | |||||
| Sprite, | |||||
| SpriteMaterial, | |||||
| SRGBColorSpace, | |||||
| Vector2, | |||||
| Vector3, | |||||
| Vector4, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {LineSegmentsGeometry} from 'three/examples/jsm/lines/LineSegmentsGeometry.js' | |||||
| import {LineSegments2} from 'three/examples/jsm/lines/LineSegments2.js' | |||||
| import {LineMaterial} from 'three/examples/jsm/lines/LineMaterial.js' | |||||
| import {onChangeDispatchEvent} from 'ts-browser-helpers' | |||||
| const [POS_X, POS_Y, POS_Z, NEG_X, NEG_Y, NEG_Z] = Array(6) | |||||
| .fill(0) | |||||
| .map((_, i) => i) | |||||
| const axesColors = [ | |||||
| new Color(0xff3653), | |||||
| new Color(0x8adb00), | |||||
| new Color(0x2c8fff), | |||||
| ] | |||||
| const clock = new Clock() | |||||
| const targetPosition = new Vector3() | |||||
| const targetQuaternion = new Quaternion() | |||||
| // const euler = new Euler() | |||||
| const q1 = new Quaternion() | |||||
| const q2 = new Quaternion() | |||||
| const point = new Vector3() | |||||
| // const dim = 128 | |||||
| const turnRate = 2 * Math.PI // turn rate in angles per second | |||||
| const raycaster = new Raycaster() | |||||
| const mouse = new Vector2() | |||||
| // const mouseStart = new Vector2() | |||||
| // const mouseAngle = new Vector2() | |||||
| const dummy = new Object3D() | |||||
| let radius = 0 | |||||
| export type GizmoOrientation = '+x' | '-x' | '+y' | '-y' | '+z' | '-z' | |||||
| export type DomPlacement = | |||||
| | 'top-left' | |||||
| | 'top-right' | |||||
| | 'top-center' | |||||
| | 'center-right' | |||||
| | 'center-left' | |||||
| | 'center-center' | |||||
| | 'bottom-left' | |||||
| | 'bottom-right' | |||||
| | 'bottom-center' | |||||
| /** | |||||
| * Extended ViewHelper implemented from the following source: | |||||
| * https://github.com/Fennec-hub/viewHelper | |||||
| * MIT License | |||||
| * Copyright (c) 2022 Fennec-hub | |||||
| */ | |||||
| export class ViewHelper2 extends Object3D { | |||||
| camera: OrthographicCamera | PerspectiveCamera | |||||
| orthoCamera = new OrthographicCamera(-1.8, 1.8, 1.8, -1.8, 0, 4) | |||||
| isViewHelper = true | |||||
| @onChangeDispatchEvent() | |||||
| animating = false | |||||
| target = new Vector3() | |||||
| backgroundSphere: Mesh | |||||
| axesLines: LineSegments2 | |||||
| spritePoints: Sprite[] | |||||
| domElement: HTMLElement | |||||
| domContainer: HTMLElement | |||||
| domRect: DOMRect | |||||
| // dragging = false | |||||
| renderer: WebGLRenderer | |||||
| // controls?: OrbitControls | TrackballControls | |||||
| // controlsChangeEvent: {listener: () => void} | |||||
| viewport: Vector4 = new Vector4() | |||||
| offsetHeight = 0 | |||||
| constructor( | |||||
| camera: PerspectiveCamera | OrthographicCamera, | |||||
| canvas: HTMLCanvasElement, | |||||
| placement: DomPlacement = 'bottom-right', | |||||
| size = 128, | |||||
| pixelRatio = 2, | |||||
| ) { | |||||
| super() | |||||
| this.renderer = new WebGLRenderer({ | |||||
| canvas: document.createElement('canvas'), | |||||
| alpha: true, | |||||
| antialias: true, | |||||
| preserveDrawingBuffer: false, | |||||
| }) | |||||
| this.renderer.setPixelRatio(pixelRatio) | |||||
| this.camera = camera | |||||
| this.domElement = canvas | |||||
| this.orthoCamera.position.set(0, 0, 2) | |||||
| this.backgroundSphere = getBackgroundSphere() | |||||
| this.axesLines = getAxesLines() | |||||
| this.spritePoints = getAxesSpritePoints() | |||||
| this.add(this.backgroundSphere, this.axesLines, ...this.spritePoints) | |||||
| this.domContainer = getDomContainer(placement, size) | |||||
| this.domContainer.appendChild(this.renderer.domElement) | |||||
| this.renderer.domElement.style.width = '100%' | |||||
| this.renderer.domElement.style.height = '100%' | |||||
| // This may cause confusion if the parent isn't the body and doesn't have a `position:relative` | |||||
| this.domElement.parentElement!.appendChild(this.domContainer) | |||||
| this.domRect = this.domContainer.getBoundingClientRect() | |||||
| this.startListening() | |||||
| // this.controlsChangeEvent = {listener: () => this.updateOrientation()} | |||||
| this.update() | |||||
| this.updateOrientation() | |||||
| } | |||||
| startListening() { | |||||
| // this.domContainer.onpointerdown = (e) => this.onPointerDown(e) | |||||
| this.domContainer.onpointermove = (e) => this.onPointerMove(e) | |||||
| this.domContainer.onpointerleave = (e) => this.onPointerLeave(e) | |||||
| this.domContainer.onclick = (e) => this.handleClick(e) | |||||
| } | |||||
| // onPointerDown(e: PointerEvent) { | |||||
| // const drag = (e1: PointerEvent) => { | |||||
| // if (!this.dragging && isClick(e1, mouseStart)) return | |||||
| // if (!this.dragging) { | |||||
| // resetSprites(this.spritePoints) | |||||
| // this.dragging = true | |||||
| // } | |||||
| // | |||||
| // mouseAngle | |||||
| // .set(e1.clientX, e1.clientY) | |||||
| // .sub(mouseStart) | |||||
| // .multiplyScalar(1 / this.domRect.width * Math.PI) | |||||
| // | |||||
| // this.rotation.x = MathUtils.clamp( | |||||
| // rotationStart.x + mouseAngle.y, | |||||
| // Math.PI / -2 + 0.001, | |||||
| // Math.PI / 2 - 0.001 | |||||
| // ) | |||||
| // this.rotation.y = rotationStart.y + mouseAngle.x | |||||
| // this.updateMatrixWorld() | |||||
| // | |||||
| // q1.copy(this.quaternion).invert() | |||||
| // | |||||
| // this.camera.position | |||||
| // .set(0, 0, 1) | |||||
| // .applyQuaternion(q1) | |||||
| // .multiplyScalar(radius) | |||||
| // .add(this.target) | |||||
| // | |||||
| // this.camera.rotation.setFromQuaternion(q1) | |||||
| // | |||||
| // this.updateOrientation(false) | |||||
| // } | |||||
| // const endDrag = () => { | |||||
| // document.removeEventListener('pointermove', drag, false) | |||||
| // document.removeEventListener('pointerup', endDrag, false) | |||||
| // | |||||
| // if (!this.dragging) { | |||||
| // // this.handleClick(e) | |||||
| // return | |||||
| // } | |||||
| // | |||||
| // this.dragging = false | |||||
| // } | |||||
| // | |||||
| // if (this.animating === true) return | |||||
| // e.preventDefault() | |||||
| // | |||||
| // mouseStart.set(e.clientX, e.clientY) | |||||
| // | |||||
| // const rotationStart = euler.copy(this.rotation) | |||||
| // | |||||
| // setRadius(this.camera, this.target) | |||||
| // | |||||
| // document.addEventListener('pointermove', drag, false) | |||||
| // document.addEventListener('pointerup', endDrag, false) | |||||
| // } | |||||
| onPointerMove(e: PointerEvent) { | |||||
| // if (this.dragging) return; | |||||
| (this.backgroundSphere.material as Material).opacity = 0.4 | |||||
| this.handleHover(e) | |||||
| this.dispatchEvent({type: 'update', event: e}) | |||||
| } | |||||
| onPointerLeave(e: PointerEvent) { | |||||
| // if (this.dragging) return; | |||||
| (this.backgroundSphere.material as Material).opacity = 0.2 | |||||
| resetSprites(this.spritePoints) | |||||
| this.domContainer.style.cursor = '' | |||||
| this.dispatchEvent({type: 'update', event: e}) | |||||
| } | |||||
| handleClick(e: PointerEvent|MouseEvent) { | |||||
| const object = getIntersectionObject( | |||||
| e, | |||||
| this.domRect, | |||||
| this.orthoCamera, | |||||
| this.spritePoints | |||||
| ) | |||||
| if (!object) return | |||||
| this.setOrientation(object.userData.type) | |||||
| } | |||||
| handleHover(e: PointerEvent) { | |||||
| const object = getIntersectionObject( | |||||
| e, | |||||
| this.domRect, | |||||
| this.orthoCamera, | |||||
| this.spritePoints | |||||
| ) | |||||
| resetSprites(this.spritePoints) | |||||
| if (!object) { | |||||
| this.domContainer.style.cursor = '' | |||||
| } else { | |||||
| object.material.map!.offset.x = 0.5 | |||||
| object.scale.multiplyScalar(1.2) | |||||
| this.domContainer.style.cursor = 'pointer' | |||||
| } | |||||
| } | |||||
| // setControls(controls?: OrbitControls | TrackballControls) { | |||||
| // if (this.controls) { | |||||
| // (this.controls as any).removeEventListener( | |||||
| // 'change', | |||||
| // this.controlsChangeEvent.listener | |||||
| // ) | |||||
| // this.target = new Vector3() | |||||
| // } | |||||
| // | |||||
| // if (!controls) return | |||||
| // | |||||
| // this.controls = controls; | |||||
| // (controls as any).addEventListener('change', this.controlsChangeEvent.listener) | |||||
| // this.target = controls.target | |||||
| // } | |||||
| render() { | |||||
| const delta = clock.getDelta() | |||||
| if (this.animating) this.animate(Math.min(delta, 1 / 30.0)) | |||||
| // const x = this.domRect.left | |||||
| // const y = this.offsetHeight - this.domRect.bottom | |||||
| const autoClear = this.renderer.autoClear | |||||
| this.renderer.autoClear = false | |||||
| // this.renderer.setViewport(x, y, dim, dim) | |||||
| this.renderer.render(this, this.orthoCamera) | |||||
| // this.renderer.setViewport(this.viewport) | |||||
| this.renderer.autoClear = autoClear | |||||
| } | |||||
| updateOrientation(fromCamera = true) { | |||||
| if (fromCamera) { | |||||
| this.quaternion.copy(this.camera.quaternion).invert() | |||||
| this.updateMatrixWorld() | |||||
| } | |||||
| updateSpritesOpacity(this.spritePoints, this.camera) | |||||
| } | |||||
| update() { | |||||
| this.domRect = this.domContainer.getBoundingClientRect() | |||||
| this.offsetHeight = this.domElement.offsetHeight | |||||
| setRadius(this.camera, this.target) | |||||
| this.renderer.getViewport(this.viewport) | |||||
| this.updateOrientation() | |||||
| } | |||||
| animate(delta: number) { | |||||
| const step = delta * turnRate | |||||
| // animate position by doing a slerp and then scaling the position on the unit sphere | |||||
| q1.rotateTowards(q2, step) | |||||
| this.camera.position | |||||
| .set(0, 0, 1) | |||||
| .applyQuaternion(q1) | |||||
| .multiplyScalar(radius) | |||||
| .add(this.target) | |||||
| // animate orientation | |||||
| this.camera.quaternion.rotateTowards(targetQuaternion, step) | |||||
| this.updateOrientation() | |||||
| if (q1.angleTo(q2) === 0) { | |||||
| this.animating = false | |||||
| } | |||||
| } | |||||
| setOrientation(orientation: GizmoOrientation) { | |||||
| prepareAnimationData(this.camera, this.target, orientation) | |||||
| this.animating = true | |||||
| this.dispatchEvent({type: 'update'}) | |||||
| } | |||||
| dispose() { | |||||
| this.axesLines.geometry.dispose(); | |||||
| (this.axesLines.material as Material).dispose() | |||||
| this.backgroundSphere.geometry.dispose(); | |||||
| (this.backgroundSphere.material as Material).dispose() | |||||
| this.spritePoints.forEach((sprite) => { | |||||
| sprite.material.map!.dispose() | |||||
| sprite.material.dispose() | |||||
| }) | |||||
| this.domContainer.remove() | |||||
| // ;(this.controls as any)?.removeEventListener( | |||||
| // 'change', | |||||
| // this.controlsChangeEvent.listener | |||||
| // ) | |||||
| } | |||||
| } | |||||
| function getDomContainer(placement: DomPlacement, size: number) { | |||||
| const div = document.createElement('div') | |||||
| const style = div.style | |||||
| style.height = `${size}px` | |||||
| style.width = `${size}px` | |||||
| style.borderRadius = '100%' | |||||
| style.position = 'absolute' | |||||
| const [y, x] = placement.split('-') | |||||
| style.transform = '' | |||||
| style.left = x === 'left' ? '0' : x === 'center' ? '50%' : '' | |||||
| style.right = x === 'right' ? '0' : '' | |||||
| style.transform += x === 'center' ? 'translateX(-50%)' : '' | |||||
| style.top = y === 'top' ? '0' : y === 'bottom' ? '' : '50%' | |||||
| style.bottom = y === 'bottom' ? '0' : '' | |||||
| style.transform += y === 'center' ? 'translateY(-50%)' : '' | |||||
| return div | |||||
| } | |||||
| function getAxesLines() { | |||||
| const distance = 0.9 | |||||
| const position = Array(3) | |||||
| .fill(0) | |||||
| .map((_, i) => [ | |||||
| !i ? distance : 0, | |||||
| i === 1 ? distance : 0, | |||||
| i === 2 ? distance : 0, | |||||
| 0, | |||||
| 0, | |||||
| 0, | |||||
| ]) | |||||
| .flat() | |||||
| const color = Array(6) | |||||
| .fill(0) | |||||
| .map((_, i) => | |||||
| i < 2 | |||||
| ? axesColors[0].toArray() | |||||
| : i < 4 | |||||
| ? axesColors[1].toArray() | |||||
| : axesColors[2].toArray() | |||||
| ) | |||||
| .flat() | |||||
| // const geometry = new BufferGeometry() | |||||
| // geometry.setAttribute( | |||||
| // 'position', | |||||
| // new BufferAttribute(new Float32Array(position), 3) | |||||
| // ) | |||||
| // geometry.setAttribute( | |||||
| // 'color', | |||||
| // new BufferAttribute(new Float32Array(color), 3) | |||||
| // ) | |||||
| const geometry = new LineSegmentsGeometry() | |||||
| geometry.setPositions(position) | |||||
| geometry.setColors(color) | |||||
| return new LineSegments2( | |||||
| geometry, | |||||
| new LineMaterial({ | |||||
| linewidth: 0.02, | |||||
| vertexColors: true, | |||||
| }) | |||||
| ) | |||||
| } | |||||
| function getBackgroundSphere() { | |||||
| const geometry = new SphereGeometry(1.6) | |||||
| const sphere = new Mesh( | |||||
| geometry, | |||||
| new MeshBasicMaterial({ | |||||
| color: 0xffffff, | |||||
| side: BackSide, | |||||
| transparent: true, | |||||
| opacity: 0.2, | |||||
| depthTest: false, | |||||
| }) | |||||
| ) | |||||
| return sphere | |||||
| } | |||||
| function getAxesSpritePoints() { | |||||
| const axes = ['x', 'y', 'z'] as const | |||||
| return Array(6) | |||||
| .fill(0) | |||||
| .map((_, i) => { | |||||
| const isPositive = i < 3 | |||||
| const sign = isPositive ? '+' : '-' | |||||
| const axis = axes[i % 3] | |||||
| const color = axesColors[i % 3] | |||||
| const sprite = new Sprite( | |||||
| getSpriteMaterial(color, isPositive ? axis : null) | |||||
| ) | |||||
| sprite.userData.type = `${sign}${axis}` | |||||
| sprite.scale.setScalar(isPositive ? 0.6 : 0.4) | |||||
| sprite.position[axis] = isPositive ? 1.2 : -1.2 | |||||
| sprite.renderOrder = 1 | |||||
| return sprite | |||||
| }) | |||||
| } | |||||
| function getSpriteMaterial(color: Color, text: 'x' | 'y' | 'z' | null = null) { | |||||
| const canvas = document.createElement('canvas') | |||||
| const padding = 0 | |||||
| const scale = 1 | |||||
| const padding2 = 0 // has a bug | |||||
| canvas.width = 128 * scale + 4 * padding + padding2 * 2 | |||||
| canvas.height = 64 * scale + 2 * padding + padding2 * 2 | |||||
| const context = canvas.getContext('2d', {alpha: true})! | |||||
| context.beginPath() | |||||
| context.arc(32 * scale + padding, 32 * scale + padding, 32 * scale - padding, 0, 2 * Math.PI) | |||||
| context.closePath() | |||||
| context.fillStyle = color.getStyle() | |||||
| context.fill() | |||||
| // for black border due to interpolation, transparent slightly bigger circle | |||||
| context.beginPath() | |||||
| context.arc(32 * scale + padding, 32 * scale + padding, 35 * scale - padding, 0, 2 * Math.PI) | |||||
| context.closePath() | |||||
| context.fillStyle = '#' + color.getHexString() + '01' | |||||
| context.fill() | |||||
| context.beginPath() | |||||
| context.arc(96 * scale + padding * 3 + padding2, 32 * scale + padding + padding2, 32 * scale - padding - padding2, 0, 2 * Math.PI) | |||||
| context.closePath() | |||||
| context.fillStyle = '#FFF' | |||||
| context.fill() | |||||
| // for black border due to interpolation, transparent slightly bigger circle | |||||
| context.beginPath() | |||||
| context.arc(96 * scale + padding * 3 + padding2, 32 * scale + padding + padding2, 35 + scale - padding - padding2, 0, 2 * Math.PI) | |||||
| context.closePath() | |||||
| context.fillStyle = '#FFFFFF01' | |||||
| context.fill() | |||||
| if (text !== null) { | |||||
| context.font = 'bold calc(44px * ' + scale + ') Arial' | |||||
| context.textAlign = 'center' | |||||
| context.fillStyle = '#111' | |||||
| context.fillText(text.toUpperCase(), 32 * scale + padding, 48 * scale + padding) | |||||
| context.fillText(text.toUpperCase(), 96 * scale + padding * 3 + padding2, 48 * scale + padding + padding2) | |||||
| } | |||||
| // canvas.style.background = '#ff0000' | |||||
| const texture = new CanvasTexture(canvas) | |||||
| texture.wrapS = texture.wrapT = RepeatWrapping | |||||
| texture.repeat.x = 0.5 | |||||
| texture.colorSpace = SRGBColorSpace | |||||
| texture.minFilter = LinearFilter | |||||
| texture.magFilter = LinearFilter | |||||
| texture.generateMipmaps = false | |||||
| texture.needsUpdate = true | |||||
| return new SpriteMaterial({ | |||||
| map: texture, | |||||
| toneMapped: false, | |||||
| transparent: true, | |||||
| }) | |||||
| } | |||||
| function prepareAnimationData( | |||||
| camera: OrthographicCamera | PerspectiveCamera, | |||||
| focusPoint: Vector3, | |||||
| axis: GizmoOrientation | |||||
| ) { | |||||
| switch (axis) { | |||||
| case '+x': | |||||
| targetPosition.set(1, 0, 0) | |||||
| targetQuaternion.setFromEuler(new Euler(0, Math.PI * 0.5, 0)) | |||||
| break | |||||
| case '+y': | |||||
| targetPosition.set(0, 1, 0) | |||||
| targetQuaternion.setFromEuler(new Euler(-Math.PI * 0.5, 0, 0)) | |||||
| break | |||||
| case '+z': | |||||
| targetPosition.set(0, 0, 1) | |||||
| targetQuaternion.setFromEuler(new Euler()) | |||||
| break | |||||
| case '-x': | |||||
| targetPosition.set(-1, 0, 0) | |||||
| targetQuaternion.setFromEuler(new Euler(0, -Math.PI * 0.5, 0)) | |||||
| break | |||||
| case '-y': | |||||
| targetPosition.set(0, -1, 0) | |||||
| targetQuaternion.setFromEuler(new Euler(Math.PI * 0.5, 0, 0)) | |||||
| break | |||||
| case '-z': | |||||
| targetPosition.set(0, 0, -1) | |||||
| targetQuaternion.setFromEuler(new Euler(0, Math.PI, 0)) | |||||
| break | |||||
| default: | |||||
| console.error('ViewHelper: Invalid axis.') | |||||
| } | |||||
| setRadius(camera, focusPoint) | |||||
| prepareQuaternions(camera, focusPoint) | |||||
| } | |||||
| function setRadius(camera: Camera, focusPoint: Vector3) { | |||||
| radius = camera.position.distanceTo(focusPoint) | |||||
| } | |||||
| function prepareQuaternions(camera: Camera, focusPoint: Vector3) { | |||||
| targetPosition.multiplyScalar(radius).add(focusPoint) | |||||
| dummy.position.copy(focusPoint) | |||||
| dummy.lookAt(camera.position) | |||||
| q1.copy(dummy.quaternion) | |||||
| dummy.lookAt(targetPosition) | |||||
| q2.copy(dummy.quaternion) | |||||
| } | |||||
| function updatePointer( | |||||
| e: PointerEvent|MouseEvent, | |||||
| domRect: DOMRect, | |||||
| orthoCamera: OrthographicCamera | |||||
| ) { | |||||
| mouse.x = (e.clientX - domRect.left) / domRect.width * 2 - 1 | |||||
| mouse.y = -((e.clientY - domRect.top) / domRect.height) * 2 + 1 | |||||
| raycaster.setFromCamera(mouse, orthoCamera) | |||||
| } | |||||
| // function isClick( | |||||
| // e: PointerEvent, | |||||
| // startCoords: Vector2, | |||||
| // threshold = 2 | |||||
| // ) { | |||||
| // return ( | |||||
| // Math.abs(e.clientX - startCoords.x) < threshold && | |||||
| // Math.abs(e.clientY - startCoords.y) < threshold | |||||
| // ) | |||||
| // } | |||||
| function getIntersectionObject( | |||||
| event: PointerEvent|MouseEvent, | |||||
| domRect: DOMRect, | |||||
| orthoCamera: OrthographicCamera, | |||||
| intersectionObjects: Sprite[] | |||||
| ) { | |||||
| updatePointer(event, domRect, orthoCamera) | |||||
| const intersects = raycaster.intersectObjects(intersectionObjects) | |||||
| if (!intersects.length) return null | |||||
| const intersection = intersects[0] | |||||
| return intersection.object as Sprite | |||||
| } | |||||
| function resetSprites(sprites: Sprite[]) { | |||||
| let i = sprites.length | |||||
| while (i--) { | |||||
| const scale = i < 3 ? 0.6 : 0.4 | |||||
| sprites[i].scale.set(scale, scale, scale) | |||||
| sprites[i].material.map!.offset.x = 1 | |||||
| } | |||||
| // sprites.forEach((sprite) => (sprite.material.map!.offset.x = 1)); | |||||
| } | |||||
| function updateSpritesOpacity(sprites: Sprite[], camera: Camera) { | |||||
| point.set(0, 0, 1) | |||||
| point.applyQuaternion(camera.quaternion) | |||||
| if (point.x >= 0) { | |||||
| sprites[POS_X].material.opacity = 1 | |||||
| sprites[NEG_X].material.opacity = 0.5 | |||||
| } else { | |||||
| sprites[POS_X].material.opacity = 0.5 | |||||
| sprites[NEG_X].material.opacity = 1 | |||||
| } | |||||
| if (point.y >= 0) { | |||||
| sprites[POS_Y].material.opacity = 1 | |||||
| sprites[NEG_Y].material.opacity = 0.5 | |||||
| } else { | |||||
| sprites[POS_Y].material.opacity = 0.5 | |||||
| sprites[NEG_Y].material.opacity = 1 | |||||
| } | |||||
| if (point.z >= 0) { | |||||
| sprites[POS_Z].material.opacity = 1 | |||||
| sprites[NEG_Z].material.opacity = 0.5 | |||||
| } else { | |||||
| sprites[POS_Z].material.opacity = 0.5 | |||||
| sprites[NEG_Z].material.opacity = 1 | |||||
| } | |||||
| } |
| export {ObjectPicker} from './ObjectPicker' | export {ObjectPicker} from './ObjectPicker' | ||||
| export {SelectionWidget, BoxSelectionWidget} from './SelectionWidget' | export {SelectionWidget, BoxSelectionWidget} from './SelectionWidget' | ||||
| export {autoGPUInstanceMeshes} from './gpu-instancing' | export {autoGPUInstanceMeshes} from './gpu-instancing' | ||||
| export {ViewHelper2, type GizmoOrientation, type DomPlacement} from './ViewHelper2' | |||||
| export {TransformControls2} from './TransformControls2' | |||||
| export {TransformControls, TransformControlsGizmo, TransformControlsPlane} from './TransformControls' | |||||
| // export {} from './constants' | // export {} from './constants' |