| @@ -95,6 +95,7 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal 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 | |||
| - [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 | |||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | |||
| - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views | |||
| @@ -110,6 +111,7 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| - [FragmentClippingExtensionPlugin](#fragmentclippingextensionplugin) - Fragment/SDF Clipping material extension for PhysicalMaterial | |||
| - [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. | |||
| - [EditorViewWidgetPlugin](#editorviewwidgetplugin) - Adds an interactive ViewHelper/AxisHelper that syncs with the main camera. | |||
| - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | |||
| - [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files | |||
| - [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files | |||
| @@ -2255,6 +2257,34 @@ pickingPlugin.addEventListener('hoverObjectChanged', (e)=>{ | |||
| ``` | |||
| ## 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 | |||
| [//]: # (todo: image) | |||
| @@ -2742,7 +2772,7 @@ import {ThreeViewer, VirtualCamerasPlugin} from 'threepipe' | |||
| 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) | |||
| camera.name = name | |||
| @@ -2758,7 +2788,31 @@ const vCam = virtualCameras.addCamera(camera) | |||
| 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 | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,35 @@ | |||
| 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) | |||
| @@ -239,8 +239,9 @@ | |||
| <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="./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="./geometry-generator-plugin/">Geometry Generator Plugin </a></li> | |||
| </ul> | |||
| <h2 class="category">Import</h2> | |||
| <ul> | |||
| @@ -298,6 +299,7 @@ | |||
| <ul> | |||
| <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="./geometry-generator-plugin/">Geometry Generator 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="./obj-to-glb/">Convert OBJ to GLB </a></li> | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,32 @@ | |||
| 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) | |||
| @@ -34,7 +34,7 @@ | |||
| <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> | |||
| <body class="code-preview-container"> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| @@ -6,6 +6,7 @@ import { | |||
| CustomBumpMapPlugin, | |||
| DepthBufferPlugin, | |||
| DropzonePlugin, | |||
| EditorViewWidgetPlugin, | |||
| FilmicGrainPlugin, | |||
| FragmentClippingExtensionPlugin, | |||
| FrameFadePlugin, | |||
| @@ -27,6 +28,7 @@ import { | |||
| STLLoadPlugin, | |||
| ThreeViewer, | |||
| TonemapPlugin, | |||
| TransformControlsPlugin, | |||
| USDZLoadPlugin, | |||
| ViewerUiConfigPlugin, | |||
| VignettePlugin, | |||
| @@ -60,6 +62,8 @@ async function init() { | |||
| new ProgressivePlugin(), | |||
| GLTFAnimationPlugin, | |||
| PickingPlugin, | |||
| TransformControlsPlugin, | |||
| EditorViewWidgetPlugin, | |||
| CameraViewPlugin, | |||
| ViewerUiConfigPlugin, | |||
| ClearcoatTintPlugin, | |||
| @@ -94,15 +98,17 @@ async function init() { | |||
| editor.loadPlugins({ | |||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | |||
| ['Interaction']: [HierarchyUiPlugin, PickingPlugin, GeometryGeneratorPlugin], | |||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin], | |||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin], | |||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | |||
| ['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin], | |||
| ['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') | |||
| // const result = await viewer.load<IObject3D>('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', { | |||
| @@ -1,18 +1,20 @@ | |||
| { | |||
| "name": "threepipe", | |||
| "version": "0.0.18", | |||
| "version": "0.0.19", | |||
| "lockfileVersion": 2, | |||
| "requires": true, | |||
| "packages": { | |||
| "": { | |||
| "name": "threepipe", | |||
| "version": "0.0.18", | |||
| "version": "0.0.19", | |||
| "license": "Apache-2.0", | |||
| "dependencies": { | |||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1018/package.tgz", | |||
| "@types/webxr": "^0.5.1", | |||
| "@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": { | |||
| "@rollup/plugin-commonjs": "^25.0.0", | |||
| @@ -38,13 +40,11 @@ | |||
| "rollup-plugin-glsl": "^1.3.0", | |||
| "rollup-plugin-license": "^3.0.1", | |||
| "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", | |||
| "tslib": "^2.5.0", | |||
| "typedoc": "^0.24.7", | |||
| "typescript": "^5.0.4", | |||
| "typescript-plugin-css-modules": "^5.0.1", | |||
| "uiconfig.js": "^0.0.9" | |||
| "typescript-plugin-css-modules": "^5.0.1" | |||
| }, | |||
| "optionalDependencies": { | |||
| "win-node-env": "^0.6.1" | |||
| @@ -8980,8 +8980,7 @@ | |||
| "node_modules/stats.js": { | |||
| "version": "0.17.0", | |||
| "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": { | |||
| "version": "1.5.0", | |||
| @@ -9599,9 +9598,9 @@ | |||
| } | |||
| }, | |||
| "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": { | |||
| "@types/wicg-file-system-access": "^2020.9.5" | |||
| } | |||
| @@ -9828,8 +9827,7 @@ | |||
| "node_modules/uiconfig.js": { | |||
| "version": "0.0.9", | |||
| "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": { | |||
| "version": "1.0.2", | |||
| @@ -16864,8 +16862,7 @@ | |||
| "stats.js": { | |||
| "version": "0.17.0", | |||
| "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", | |||
| "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", | |||
| "dev": true | |||
| "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" | |||
| }, | |||
| "statuses": { | |||
| "version": "1.5.0", | |||
| @@ -17329,9 +17326,9 @@ | |||
| "dev": true | |||
| }, | |||
| "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": { | |||
| "@types/wicg-file-system-access": "^2020.9.5" | |||
| } | |||
| @@ -17503,8 +17500,7 @@ | |||
| "uiconfig.js": { | |||
| "version": "0.0.9", | |||
| "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": { | |||
| "version": "1.0.2", | |||
| @@ -21,6 +21,8 @@ export {SceneUiConfigPlugin} from './ui/SceneUiConfigPlugin' | |||
| export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin' | |||
| export {FullScreenPlugin} from './interaction/FullScreenPlugin' | |||
| export {PickingPlugin} from './interaction/PickingPlugin' | |||
| export {TransformControlsPlugin} from './interaction/TransformControlsPlugin' | |||
| export {EditorViewWidgetPlugin} from './interaction/EditorViewWidgetPlugin' | |||
| // import | |||
| export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' | |||
| @@ -0,0 +1,78 @@ | |||
| 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) | |||
| } | |||
| } | |||
| @@ -51,6 +51,7 @@ export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'ho | |||
| this._widget?.attach(this._picker.selectedObject) | |||
| else | |||
| this._widget?.detach() | |||
| this.uiConfig?.uiRefresh?.(true) | |||
| } | |||
| public setDirty() { | |||
| @@ -0,0 +1,97 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,79 @@ | |||
| /* 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; | |||
| } | |||
| @@ -0,0 +1,185 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,658 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -9,5 +9,8 @@ export {threeConstMappings} from './const-mappings' | |||
| export {ObjectPicker} from './ObjectPicker' | |||
| export {SelectionWidget, BoxSelectionWidget} from './SelectionWidget' | |||
| 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' | |||