├── src2 ├── ManipulatorMode.js ├── state │ ├── ScaleState.js │ ├── PositionState.js │ ├── ManipulatorState.js │ └── RotationState.js ├── Manipulator3D.js ├── MouseEvents.js ├── ManipulatorActions.js └── Tracer.js ├── test ├── _lib │ ├── Starter.css │ ├── FacedCube.js │ ├── Util.js │ ├── DynamicMesh.js │ ├── Starter.js │ └── DynLineMesh.js ├── test_no_attachment.html ├── test_dist.html ├── test.html ├── states.html ├── ManipulatorDebugger.js ├── new2.html ├── new.html └── annotate_box.html ├── src3 ├── Structs.js ├── lib │ ├── EventDispatcher.js │ ├── Func.js │ ├── StateProxy.js │ ├── MouseEvents.js │ ├── transform.js │ └── RayIntersection.js ├── mod │ ├── Plane.js │ ├── Translation.js │ ├── Scale.js │ └── Rotation.js ├── render │ ├── FloorCube.js │ └── Mesh3JS.js ├── DragAction.js ├── Manipulator3D copy.js ├── Actions.js └── Manipulator3D.js ├── src ├── index.js ├── RayIntersection.js ├── ManipulatorMesh.js └── Maths.js ├── annotatebox ├── Structs.js ├── lib │ ├── EventDispatcher.js │ ├── Func.js │ ├── StateProxy.js │ └── MouseEvents.js ├── mod │ ├── Translation.js │ ├── Rotation.js │ └── Faces.js ├── DragAction.js ├── Actions.js ├── AnnotateBox.js └── render │ ├── Mesh3js.js │ └── ScaleCube.js ├── notes.txt ├── LICENSE ├── package.json ├── .gitignore ├── vite.config.js └── README.md /src2/ManipulatorMode.js: -------------------------------------------------------------------------------- 1 | const ManipulatorMode = Object.freeze({ 2 | None : -1, 3 | Translate : 0, 4 | Rotate : 1, 5 | Scale : 2, 6 | }); 7 | 8 | export default ManipulatorMode; -------------------------------------------------------------------------------- /test/_lib/Starter.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: #fff; 5 | font-family: monospace; 6 | font-size: 12px; 7 | overscroll-behavior: none; 8 | } 9 | canvas{ display: block; } -------------------------------------------------------------------------------- /src2/state/ScaleState.js: -------------------------------------------------------------------------------- 1 | export default class ScaleState{ 2 | value = [1,1,1]; 3 | set( v ){ 4 | this.value[ 0 ] = v[ 0 ]; 5 | this.value[ 1 ] = v[ 1 ]; 6 | this.value[ 2 ] = v[ 2 ]; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src3/Structs.js: -------------------------------------------------------------------------------- 1 | const Modes = Object.freeze({ 2 | None : -1, 3 | Translate : 0, 4 | Rotate : 1, 5 | Scale : 2, 6 | Plane : 3, 7 | }); 8 | 9 | const Axes = Object.freeze({ 10 | None : -1, 11 | X : 0, 12 | Y : 1, 13 | Z : 2, 14 | All : 3, 15 | }) 16 | 17 | export { Modes, Axes }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ManipulatorData, ManipulatorMode } from './ManipulatorData.js'; 2 | import { ManipulatorMesh } from './ManipulatorMesh.js'; 3 | import { Manipulator3D } from './Manipulator3D.js'; 4 | 5 | export { 6 | Manipulator3D, 7 | ManipulatorData, 8 | ManipulatorMode, 9 | ManipulatorMesh, 10 | }; -------------------------------------------------------------------------------- /annotatebox/Structs.js: -------------------------------------------------------------------------------- 1 | const Modes = Object.freeze({ 2 | None : -1, 3 | Translate : 0, 4 | Rotate : 1, 5 | Scale : 2, 6 | }); 7 | 8 | const Axes = Object.freeze({ 9 | None : -1, 10 | X : 0, 11 | Y : 1, 12 | Z : 2, 13 | XN : 3, 14 | YN : 4, 15 | ZN : 5, 16 | }) 17 | 18 | export { Modes, Axes }; -------------------------------------------------------------------------------- /src3/lib/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | export default function EventDispatcher(){ 2 | const evtTarget = new EventTarget(); 3 | return { 4 | on( evtName, fn ){ evtTarget.addEventListener( evtName, fn ); return this; }, 5 | off( evtName, fn ){ evtTarget.removeEventListener( evtName, fn ); return this; }, 6 | once( evtName, fn ){ evtTarget.addEventListener( evtName, fn, { once:true } ); return this; }, 7 | emit( evtName, data ){ 8 | evtTarget.dispatchEvent( new CustomEvent( evtName, { detail:data, bubbles: false, cancelable:true, composed:false } ) ); 9 | return this; 10 | }, 11 | }; 12 | } -------------------------------------------------------------------------------- /annotatebox/lib/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | export default function EventDispatcher(){ 2 | const evtTarget = new EventTarget(); 3 | return { 4 | on( evtName, fn ){ evtTarget.addEventListener( evtName, fn ); return this; }, 5 | off( evtName, fn ){ evtTarget.removeEventListener( evtName, fn ); return this; }, 6 | once( evtName, fn ){ evtTarget.addEventListener( evtName, fn, { once:true } ); return this; }, 7 | emit( evtName, data ){ 8 | evtTarget.dispatchEvent( new CustomEvent( evtName, { detail:data, bubbles: false, cancelable:true, composed:false } ) ); 9 | return this; 10 | }, 11 | }; 12 | } -------------------------------------------------------------------------------- /src3/lib/Func.js: -------------------------------------------------------------------------------- 1 | export function debounce( fn, delay ){ 2 | let id = null; 3 | return ()=>{ //...args 4 | if( id ) clearTimeout( id ); 5 | id = setTimeout( ()=>fn.apply( null, arguments ), delay ); //id = setTimeout( ()=>fn( ...args ), delay ); 6 | }; 7 | } 8 | 9 | export function memorize( fn ){ 10 | const cache = {}; 11 | return ( ...args )=>{ 12 | const key = args.toString(); 13 | if( key in cache ) return cache[ key ]; 14 | 15 | const result = fn( ...args ); 16 | cache[ key ] = result; 17 | return result; 18 | } 19 | } 20 | 21 | export function throttle( fn, delay ){ 22 | let lastTime = 0; 23 | return ( ...args )=>{ 24 | const now = new Date().getTime(); 25 | if( now - lastTime < delay ) return; 26 | lastTime = now; 27 | fn( ...args ); 28 | } 29 | } -------------------------------------------------------------------------------- /annotatebox/lib/Func.js: -------------------------------------------------------------------------------- 1 | export function debounce( fn, delay ){ 2 | let id = null; 3 | return ()=>{ //...args 4 | if( id ) clearTimeout( id ); 5 | id = setTimeout( ()=>fn.apply( null, arguments ), delay ); //id = setTimeout( ()=>fn( ...args ), delay ); 6 | }; 7 | } 8 | 9 | export function memorize( fn ){ 10 | const cache = {}; 11 | return ( ...args )=>{ 12 | const key = args.toString(); 13 | if( key in cache ) return cache[ key ]; 14 | 15 | const result = fn( ...args ); 16 | cache[ key ] = result; 17 | return result; 18 | } 19 | } 20 | 21 | export function throttle( fn, delay ){ 22 | let lastTime = 0; 23 | return ( ...args )=>{ 24 | const now = new Date().getTime(); 25 | if( now - lastTime < delay ) return; 26 | lastTime = now; 27 | fn( ...args ); 28 | } 29 | } -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://codesandbox.io/s/object-gizmo-controls-om2ff8?file=/src/pivot/AxisRotator.js:3237-3260 4 | Nice widget, Very interested in the pivot rotation 5 | 6 | const { clickPoint, origin, e1, e2, normal, plane } = clickInfo.current 7 | ray.copy(e.ray) 8 | ray.intersectPlane(plane, intersection) 9 | ray.direction.negate() 10 | ray.intersectPlane(plane, intersection) 11 | let angle = calculateAngle(clickPoint, intersection, origin, e1, e2) 12 | let degrees = toDegrees(angle) 13 | 14 | if (e.shiftKey) { 15 | degrees = Math.round(degrees / 10) * 10 16 | angle = toRadians(degrees) 17 | } 18 | 19 | divRef.current.innerText = degrees.toFixed(0) + 'º' 20 | 21 | rotMatrix.makeRotationAxis(normal, angle) 22 | posNew.copy(origin).applyMatrix4(rotMatrix).sub(origin).negate() 23 | rotMatrix.setPosition(posNew) 24 | onDrag(rotMatrix) -------------------------------------------------------------------------------- /test/_lib/FacedCube.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | export default function facedCube( pos=null, scl=null ){ 3 | const geo = new THREE.BoxGeometry( 1, 1, 1 ); 4 | const mat = [ 5 | //new THREE.MeshBasicMaterial( { color: 0x00ff00 } ), // Left 6 | new THREE.MeshBasicMaterial( { color: 0xeeeeee } ), // Left 7 | new THREE.MeshBasicMaterial( { color: 0x777777 } ), // Right 8 | // new THREE.MeshBasicMaterial( { color: 0x0000ff } ), // Top 9 | new THREE.MeshBasicMaterial( { color: 0xaaaaaa } ), // Top 10 | new THREE.MeshBasicMaterial( { color: 0x222222 } ), // Bottom 11 | // new THREE.MeshBasicMaterial( { color: 0xff0000 } ), // Forward 12 | new THREE.MeshBasicMaterial( { color: 0x999999 } ), // Forward 13 | new THREE.MeshBasicMaterial( { color: 0xffffff } ), // Back 14 | ]; 15 | 16 | const mesh = new THREE.Mesh( geo, mat ); 17 | 18 | if( pos ) mesh.position.fromArray( pos ); 19 | if( scl != null ) mesh.scale.set( scl, scl, scl ); 20 | 21 | return mesh; 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sketchpunk Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "manipulator3d", 3 | "version" : "0.0.8", 4 | "author" : "Pedro Sousa ( Vor @ SketchPunk Labs )", 5 | "description" : "3D Manipulator Gizmo for Threejs", 6 | "keywords" : [ "threejs", "gizmo", "ui", "3d" ], 7 | "license" : "MIT", 8 | "homepage" : "https://github.com/sketchpunklabs/manipulator3d#readme", 9 | "repository" : { "type": "git", "url": "git+https://github.com/sketchpunklabs/manipulator3d.git" }, 10 | "bugs" : { "url": "https://github.com/sketchpunklabs/manipulator3d/issues" }, 11 | "files" : [ "dist" ], 12 | "main" : "./dist/manipulator3d.cjs.js", 13 | "module" : "./dist/manipulator3d.es.js", 14 | "exports": { 15 | ".": { 16 | "import" : "./dist/manipulator3d.es.js", 17 | "require" : "./dist/manipulator3d.cjs.js" 18 | } 19 | }, 20 | "scripts": { 21 | "dev" : "vite", 22 | "build" : "vite build", 23 | "publish" : "npm publish --access public" 24 | }, 25 | 26 | "peerDependencies": { 27 | "three" : "^0.138.2" 28 | }, 29 | 30 | "devDependencies": { 31 | "vite": "^2.8.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src3/mod/Plane.js: -------------------------------------------------------------------------------- 1 | import { vec3_add, vec3_sub } from '../lib/Maths.js'; 2 | import { 3 | // Ray, 4 | intersectPlane, 5 | intersectTri, 6 | // intersectSphere, 7 | //nearPoint, 8 | //NearSegmentResult, 9 | //nearSegment, 10 | } from '../../src/RayIntersection.js'; 11 | import { vec3_copy, vec3_scaleAndAdd } from '../lib/Maths.js'; 12 | 13 | 14 | export default class Plane{ 15 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 16 | const a = [ 0, 0, 0 ]; 17 | const b = [ 0, 0, 0 ]; 18 | const c = [ 0, 0, 0 ]; 19 | const x = 0.15 * state.cameraScale; 20 | const y = 0.65 * state.cameraScale; 21 | const origin = state.position; 22 | 23 | for( let i=0; i < 3; i++ ){ 24 | let ii = ( i + 1 ) % 3; 25 | let iii = ( i + 2 ) % 3; 26 | 27 | vec3_scaleAndAdd( a, origin, axes[ ii ], x ); 28 | vec3_scaleAndAdd( a, a, axes[ iii ], x ); 29 | 30 | vec3_scaleAndAdd( b, a, axes[ ii ], y ); 31 | vec3_scaleAndAdd( c, a, axes[ iii ], y ); 32 | 33 | if( intersectTri( ray, b, a, c, hitPos, false ) ){ 34 | return i; 35 | } 36 | } 37 | 38 | return -1; 39 | } 40 | 41 | static rayDrag( ray, origin, axes, iAxis, initHit ){ 42 | const t = intersectPlane( ray, origin, axes[ iAxis ] ); 43 | if( t === null ) return null; 44 | 45 | const offset = vec3_sub( [0,0,0], origin, initHit ); 46 | const pos = ray.posAt( t ); 47 | 48 | return vec3_add( pos, pos, offset ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /annotatebox/mod/Translation.js: -------------------------------------------------------------------------------- 1 | import { vec3_add, vec3_sub } from '../lib/Maths.js'; 2 | import { 3 | // Ray, 4 | // intersectPlane, 5 | // intersectTri, 6 | // intersectSphere, 7 | // nearPoint, 8 | NearSegmentResult, 9 | nearSegment, 10 | } from '../lib/RayIntersection.js'; 11 | import { vec3_copy, vec3_scaleAndAdd } from '../lib/Maths.js'; 12 | 13 | 14 | export default class Translation{ 15 | static minRayDistance = 0.08; 16 | 17 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 18 | const segResult = new NearSegmentResult(); 19 | const a = [ 0, 0, 0 ]; 20 | const b = [ 0, 0, 0 ]; 21 | const p = state.position; 22 | 23 | let minDist = Infinity; 24 | let minAxis = -1; 25 | 26 | for( let i=0; i < 3; i++ ){ 27 | vec3_scaleAndAdd( a, p, axes[i], state.axesLengths[i] ); 28 | vec3_scaleAndAdd( b, a, axes[i], 1.0 ); 29 | 30 | if( nearSegment( ray, a, b, segResult ) ){ 31 | if( segResult.distance <= this.minRayDistance && segResult.distance < minDist ){ 32 | minAxis = i; 33 | minDist = segResult.distance; 34 | vec3_copy( hitPos, segResult.segPosition ); 35 | } 36 | } 37 | } 38 | 39 | return minAxis; 40 | } 41 | 42 | static rayDrag( ray, origin, axis, initHit ){ 43 | const a = vec3_scaleAndAdd( [0,0,0], origin, axis, 1000 ); 44 | const b = vec3_scaleAndAdd( [0,0,0], origin, axis, -1000 ); 45 | const segResult = new NearSegmentResult(); 46 | 47 | if( nearSegment( ray, a, b, segResult ) ){ 48 | const offset = vec3_sub( [0,0,0], origin, initHit ); 49 | vec3_add( offset, offset, segResult.segPosition ); 50 | return offset; 51 | } 52 | 53 | return null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src3/mod/Translation.js: -------------------------------------------------------------------------------- 1 | import { vec3_add, vec3_sub } from '../lib/Maths.js'; 2 | import { 3 | // Ray, 4 | // intersectPlane, 5 | // intersectTri, 6 | // intersectSphere, 7 | // nearPoint, 8 | NearSegmentResult, 9 | nearSegment, 10 | } from '../../src/RayIntersection.js'; 11 | import { vec3_copy, vec3_scaleAndAdd } from '../lib/Maths.js'; 12 | 13 | 14 | export default class Translation{ 15 | static minRayDistance = 0.08; 16 | 17 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 18 | const minRange = this.minRayDistance * state.cameraScale; 19 | const segResult = new NearSegmentResult(); 20 | const a = [ 0, 0, 0 ]; 21 | const b = [ 0, 0, 0 ]; 22 | const s = state.axisLength * state.cameraScale; 23 | const p = state.position; 24 | 25 | let minDist = Infinity; 26 | let minAxis = -1; 27 | 28 | for( let i=0; i < 3; i++ ){ 29 | vec3_scaleAndAdd( a, p, axes[i], -s ); 30 | vec3_scaleAndAdd( b, p, axes[i], s ); 31 | 32 | if( nearSegment( ray, a, b, segResult ) ){ 33 | if( segResult.distance <= minRange && segResult.distance < minDist ){ 34 | minAxis = i; 35 | minDist = segResult.distance; 36 | vec3_copy( hitPos, segResult.segPosition ); 37 | } 38 | } 39 | } 40 | 41 | return minAxis; 42 | } 43 | 44 | static rayDrag( ray, origin, axis, initHit ){ 45 | const a = vec3_scaleAndAdd( [0,0,0], origin, axis, 1000 ); 46 | const b = vec3_scaleAndAdd( [0,0,0], origin, axis, -1000 ); 47 | const segResult = new NearSegmentResult(); 48 | 49 | if( nearSegment( ray, a, b, segResult ) ){ 50 | const offset = vec3_sub( [0,0,0], origin, initHit ); 51 | vec3_add( offset, offset, segResult.segPosition ); 52 | return offset; 53 | } 54 | 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src3/lib/StateProxy.js: -------------------------------------------------------------------------------- 1 | export default class StateProxy{ 2 | // #region MAIN 3 | static new( data={} ){ return new Proxy( data, new StateProxy( data ) ); } 4 | 5 | constructor( data ){ 6 | this._data = data; 7 | this._events = new EventTarget(); 8 | } 9 | // #endregion 10 | 11 | // #region METHODS 12 | update( struct, emitChange=false ){ 13 | Object.assign( this._data, struct ); 14 | if( emitChange ) this.emit( 'change', null ); 15 | return this; 16 | } 17 | // #endregion 18 | 19 | // #region PROXY TRAPS 20 | get( target, prop, receiver ){ 21 | // console.log( "GET", "target", target, "prop", prop, "rec", receiver ); 22 | if( prop === '$' ) return this; 23 | return Reflect.get( target, prop, receiver ); //target[ prop ]; 24 | } 25 | 26 | set( target, prop, value ){ 27 | // console.log( "SET", "target", target, "prop", prop, "value", value, 'prev', Reflect.get( target, prop ) ); 28 | if( prop === '$' ) return false; 29 | 30 | // Only update the state if the value is different 31 | if( Reflect.get( target, prop ) !== value ){ 32 | Reflect.set( target, prop, value ); // Save data to Object 33 | this.emit( prop + 'Change', value ); // Emit event that property changed 34 | this.emit( 'change', { prop, value } ); // Emit event that any property changed 35 | } 36 | return true; 37 | } 38 | // #endregion 39 | 40 | // #region EVENTS 41 | on( evtName, fn ){ this._events.addEventListener( evtName, fn ); return this; } 42 | off( evtName, fn){ this._events.removeEventListener( evtName, fn ); return this; } 43 | once( evtName, fn ){ this._events.addEventListener( evtName, fn, { once:true } ); return this; } 44 | emit( evtName, data ){ 45 | this._events.dispatchEvent( new CustomEvent( evtName, { detail:data, bubbles: false, cancelable:true, composed:false } ) ); 46 | return this; 47 | } 48 | // #endregion 49 | } -------------------------------------------------------------------------------- /annotatebox/lib/StateProxy.js: -------------------------------------------------------------------------------- 1 | export default class StateProxy{ 2 | // #region MAIN 3 | static new( data={} ){ return new Proxy( data, new StateProxy( data ) ); } 4 | 5 | constructor( data ){ 6 | this._data = data; 7 | this._events = new EventTarget(); 8 | } 9 | // #endregion 10 | 11 | // #region METHODS 12 | update( struct, emitChange=false ){ 13 | Object.assign( this._data, struct ); 14 | if( emitChange ) this.emit( 'change', null ); 15 | return this; 16 | } 17 | // #endregion 18 | 19 | // #region PROXY TRAPS 20 | get( target, prop, receiver ){ 21 | // console.log( "GET", "target", target, "prop", prop, "rec", receiver ); 22 | if( prop === '$' ) return this; 23 | return Reflect.get( target, prop, receiver ); //target[ prop ]; 24 | } 25 | 26 | set( target, prop, value ){ 27 | // console.log( "SET", "target", target, "prop", prop, "value", value, 'prev', Reflect.get( target, prop ) ); 28 | if( prop === '$' ) return false; 29 | 30 | // Only update the state if the value is different 31 | if( Reflect.get( target, prop ) !== value ){ 32 | Reflect.set( target, prop, value ); // Save data to Object 33 | this.emit( prop + 'Change', value ); // Emit event that property changed 34 | this.emit( 'change', { prop, value } ); // Emit event that any property changed 35 | } 36 | return true; 37 | } 38 | // #endregion 39 | 40 | // #region EVENTS 41 | on( evtName, fn ){ this._events.addEventListener( evtName, fn ); return this; } 42 | off( evtName, fn){ this._events.removeEventListener( evtName, fn ); return this; } 43 | once( evtName, fn ){ this._events.addEventListener( evtName, fn, { once:true } ); return this; } 44 | emit( evtName, data ){ 45 | this._events.dispatchEvent( new CustomEvent( evtName, { detail:data, bubbles: false, cancelable:true, composed:false } ) ); 46 | return this; 47 | } 48 | // #endregion 49 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | -------------------------------------------------------------------------------- /src3/mod/Scale.js: -------------------------------------------------------------------------------- 1 | import { vec3_add, vec3_sub, vec3_dot, vec3_norm } from '../lib/Maths.js'; 2 | import { 3 | // Ray, 4 | // intersectPlane, 5 | // intersectTri, 6 | // intersectSphere, 7 | nearPoint, 8 | NearSegmentResult, 9 | nearSegment, 10 | } from '../../src/RayIntersection.js'; 11 | import { vec3_copy, vec3_scaleAndAdd, vec3_len } from '../lib/Maths.js'; 12 | 13 | 14 | export default class Scale{ 15 | static minRayDistance = 0.08; 16 | 17 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 18 | const minRange = this.minRayDistance * state.cameraScale; 19 | const a = [ 0, 0, 0 ]; 20 | const b = [ 0, 0, 0 ]; 21 | const s = state.pntRadius * state.cameraScale; 22 | const origin = state.position; 23 | 24 | if( nearPoint( ray, origin, minRange ) !== null ) return 3; 25 | 26 | for( let i=0; i < 3; i++ ){ 27 | vec3_scaleAndAdd( a, origin, axes[i], -s ); 28 | if( nearPoint( ray, a, minRange ) !== null ){ 29 | vec3_copy( hitPos, a ); 30 | return i; 31 | } 32 | 33 | vec3_scaleAndAdd( b, origin, axes[i], s ); 34 | if( nearPoint( ray, b, minRange ) !== null ){ 35 | vec3_copy( hitPos, b ); 36 | return i; 37 | } 38 | } 39 | 40 | return -1; 41 | } 42 | 43 | static rayDrag( ray, origin, axes, iAxis, initHit ){ 44 | if( iAxis === 3 ) return null; 45 | 46 | const a = vec3_scaleAndAdd( [0,0,0], origin, axes[ iAxis ], 1000 ); 47 | const b = vec3_scaleAndAdd( [0,0,0], origin, axes[ iAxis ], -1000 ); 48 | const result = new NearSegmentResult(); 49 | 50 | if( nearSegment( ray, a, b, result ) ){ 51 | const dir = vec3_sub( [0,0,0], result.segPosition, initHit ); 52 | const len = vec3_len( dir ); 53 | const sign = Math.sign( vec3_dot( dir, axes[ iAxis ] ) ); 54 | 55 | const out = [0,0,0]; 56 | out[ iAxis ] = len * sign; 57 | 58 | return out; 59 | } 60 | 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | const ignorePaths = [ ".git", "node_modules", "dist", "site" ]; 5 | 6 | function getHtmlPaths( dirPath = __dirname, htmlPaths = {} ){ 7 | const files = fs.readdirSync(dirPath); 8 | 9 | for( const file of files ){ 10 | if( ignorePaths.includes( file ) ) continue; 11 | 12 | const absPath = path.join( dirPath, file ); 13 | 14 | if( fs.statSync(absPath).isDirectory() ){ 15 | htmlPaths = getHtmlPaths( absPath, htmlPaths ); 16 | 17 | }else if( path.extname(file) === ".html" ){ 18 | const relPath = path.relative( __dirname, absPath ); 19 | htmlPaths[relPath] = absPath; 20 | } 21 | } 22 | 23 | return htmlPaths; 24 | } 25 | 26 | export default ( { command, mode } ) => { 27 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | if( mode === "site" || command === "serve" ){ 29 | 30 | const repo = process.env.GITHUB_REPOSITORY; 31 | const base = ( repo )? `/${repo.split("/")[1]}/` : '/'; 32 | 33 | return { 34 | base, 35 | build : { 36 | outDir : path.resolve( __dirname, "site" ), 37 | minify : false, 38 | rollupOptions : { input: getHtmlPaths() }, 39 | }, 40 | publicDir : path.join( __dirname, "test", "public" ), 41 | server : { 42 | port : 3012, 43 | open : '/test/test.html', 44 | }, 45 | }; 46 | 47 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | } else { 49 | return { 50 | build: { 51 | minify : false, 52 | 53 | lib : { 54 | entry : path.resolve( __dirname, "src/index.js" ), 55 | name : "Manipulator3D", 56 | formats : [ "es", "cjs" ], 57 | }, 58 | 59 | rollupOptions : { 60 | external : [ "three", /^three\// ], 61 | } 62 | } 63 | }; 64 | } 65 | }; -------------------------------------------------------------------------------- /test/test_no_attachment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 62 | -------------------------------------------------------------------------------- /test/_lib/Util.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default class Util{ 4 | static facedCube( pos=null, scl=null, size=[1,1,1] ){ 5 | const geo = new THREE.BoxGeometry( size[0], size[1], size[2] ); 6 | const mat = [ 7 | new THREE.MeshBasicMaterial( { color: 0x00ff00 } ), // Left 8 | new THREE.MeshBasicMaterial( { color: 0x777777 } ), // Right 9 | new THREE.MeshBasicMaterial( { color: 0x0000ff } ), // Top 10 | new THREE.MeshBasicMaterial( { color: 0x222222 } ), // Bottom 11 | new THREE.MeshBasicMaterial( { color: 0xff0000 } ), // Forward 12 | new THREE.MeshBasicMaterial( { color: 0xffffff } ), // Back 13 | ]; 14 | 15 | const mesh = new THREE.Mesh( geo, mat ); 16 | 17 | if( pos ) mesh.position.fromArray( pos ); 18 | if( scl != null ) mesh.scale.set( scl, scl, scl ); 19 | 20 | return mesh; 21 | } 22 | 23 | static mesh( verts, idx=null, norm=null, uv=null, mat=null, wireFrame=false ){ 24 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | const bGeo = new THREE.BufferGeometry(); 26 | bGeo.setAttribute( "position", new THREE.BufferAttribute( new Float32Array( verts ), 3 ) ); 27 | 28 | if( idx && idx.length > 0 ) bGeo.setIndex( idx ); 29 | if( norm && norm.length > 0 ) bGeo.setAttribute( "normal", new THREE.BufferAttribute( new Float32Array( norm ), 3 ) ); 30 | if( uv && uv.length > 0 ) bGeo.setAttribute( "uv", new THREE.BufferAttribute( new Float32Array( uv ), 2 ) ); 31 | 32 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 33 | mat = mat || new THREE.MeshPhongMaterial( { color:0x009999 } ); // ,side:THREE.DoubleSide 34 | const mesh = new THREE.Mesh( bGeo, mat ); 35 | 36 | if( wireFrame ){ 37 | const mat = new THREE.LineBasicMaterial({ color:0xffffff, opacity:0.6, transparent:true }); 38 | const wGeo = new THREE.WireframeGeometry( bGeo ); 39 | const grp = new THREE.Group(); 40 | grp.add( mesh ); 41 | grp.add( new THREE.LineSegments( wGeo, mat ) ) 42 | return grp; 43 | }else{ 44 | return mesh; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/test_dist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 61 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 62 | -------------------------------------------------------------------------------- /src3/mod/Rotation.js: -------------------------------------------------------------------------------- 1 | import { vec3_add, vec3_angle, vec3_norm } from '../lib/Maths.js'; 2 | import { 3 | // Ray, 4 | intersectPlane, 5 | // intersectTri, 6 | // intersectSphere, 7 | // nearPoint, 8 | //NearSegmentResult, 9 | //nearSegment, 10 | } from '../../src/RayIntersection.js'; 11 | 12 | import { 13 | vec3_sub, 14 | vec3_len, 15 | vec3_dot, 16 | //vec3_scaleAndAdd, 17 | quat_rotateTo, 18 | } from '../lib/Maths.js'; 19 | import { ZeroCurvatureEnding } from 'three'; 20 | import { Axes } from '../Structs.js'; 21 | 22 | 23 | export default class Rotation{ 24 | static minRayDistance = 0.08; 25 | 26 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 27 | const minRange = this.minRayDistance * state.cameraScale; 28 | const origin = state.position; 29 | const radius = state.arcRadius * state.cameraScale; 30 | const hitDir = [0,0,0]; 31 | let t; 32 | let dist; 33 | 34 | for( let i=0; i < 3; i++ ){ 35 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | // First test against the plane using the axis as the plane normal 37 | t = intersectPlane( ray, origin, axes[i] ); 38 | if( t === null ) continue; 39 | 40 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | // Next do a circle radius test of the hit point to plane origin 42 | ray.posAt( t, hitPos ); 43 | dist = vec3_len( hitPos, origin ); 44 | 45 | if( Math.abs( radius - dist ) <= minRange ){ 46 | 47 | // Test if in the correct quadrant 48 | vec3_sub( hitDir, hitPos, origin ); 49 | if( vec3_dot( hitDir, axes[ ( i + 1 ) % 3 ] ) >= 0 && 50 | vec3_dot( hitDir, axes[ ( i + 2 ) % 3 ] ) >= 0 ){ 51 | return i; 52 | } 53 | 54 | } 55 | } 56 | 57 | return -1; 58 | } 59 | 60 | static rayDrag( ray, origin, axes, iAxis, hitPos ){ 61 | let t = intersectPlane( ray, origin, axes[ iAxis ] ); 62 | if( t === null ) return null; 63 | 64 | const fromDir = vec3_sub( [0,0,0], hitPos, origin ); 65 | vec3_norm( fromDir, fromDir ); 66 | 67 | const toDir = vec3_sub( [0,0,0], ray.posAt( t ), origin ); 68 | vec3_norm( toDir, toDir ); 69 | 70 | return quat_rotateTo( [0,0,0,1], fromDir, toDir ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src2/state/PositionState.js: -------------------------------------------------------------------------------- 1 | import { 2 | vec3_add, 3 | // vec3_sub, 4 | // vec3_len, 5 | vec3_copy, 6 | // vec3_transformQuat, 7 | // vec3_norm, 8 | vec3_dot, 9 | // vec3_scale, 10 | // vec3_scaleAndAdd, 11 | // vec3_sqrLen, 12 | // quat_mul, 13 | // quat_normalize, 14 | // quat_copy, 15 | // quat_sqrLen, 16 | // quat_setAxisAngle, 17 | } from '../../src/Maths.js'; 18 | 19 | 20 | import { 21 | // Ray, 22 | // intersectPlane, 23 | // intersectTri, 24 | // intersectSphere, 25 | // nearPoint, 26 | NearSegmentResult, 27 | nearSegment, 28 | } from '../../src/RayIntersection.js'; 29 | 30 | export default class PositionState{ 31 | // #region DATA 32 | value = [0,0,0]; 33 | 34 | minRayDistance = 0.2; 35 | xEndPoint = [0,0,0]; 36 | yEndPoint = [0,0,0]; 37 | zEndPoint = [0,0,0]; 38 | 39 | aryEndPoint = [ 40 | this.xEndPoint, 41 | this.yEndPoint, 42 | this.zEndPoint, 43 | ]; 44 | // #endregion 45 | 46 | set( v ){ 47 | this.value[ 0 ] = v[ 0 ]; 48 | this.value[ 1 ] = v[ 1 ]; 49 | this.value[ 2 ] = v[ 2 ]; 50 | } 51 | 52 | updateEndpoints( man ){ 53 | vec3_add( this.xEndPoint, this.value, man.rotation.xAxisScl ); 54 | vec3_add( this.yEndPoint, this.value, man.rotation.yAxisScl ); 55 | vec3_add( this.zEndPoint, this.value, man.rotation.zAxisScl ); 56 | } 57 | 58 | rayTest( ray, state, hitPos=[0,0,0] ){ 59 | const minRange = state.cameraScale * this.minRayDistance; 60 | const segResult = new NearSegmentResult(); 61 | let minDist = Infinity; 62 | let minAxis = -1; 63 | 64 | //Debug.pnt2.reset(); 65 | 66 | for( let i=0; i < 3; i++ ){ 67 | if( nearSegment( ray, this.value, this.aryEndPoint[i], segResult ) ){ 68 | if( ( segResult.distance <= minRange ) && segResult.distance < minDist ){ 69 | minAxis = i; 70 | minDist = segResult.distance; 71 | vec3_copy( hitPos, segResult.segPosition ); 72 | } 73 | } 74 | } 75 | 76 | // if( minDist !== Infinity ){ 77 | // // console.log( 'Axis', minAxis, 'at', minPos ); 78 | // // Debug.pnt2.add( minPos, 0x00ff00, 4 ) 79 | // return true; 80 | // } 81 | 82 | return minAxis; 83 | } 84 | } -------------------------------------------------------------------------------- /test/states.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 68 | -------------------------------------------------------------------------------- /annotatebox/DragAction.js: -------------------------------------------------------------------------------- 1 | import { 2 | // vec3_add, 3 | // vec3_sub, 4 | // vec3_len, 5 | // vec3_norm, 6 | // vec3_scale, 7 | vec3_inv_scale, 8 | vec3_transformQuat, 9 | // vec3_sqrLen, 10 | vec3_copy, 11 | quat_mul, 12 | quat_copy, 13 | vec3_add, 14 | vec3_scaleAndAdd, 15 | //quat_setAxisAngle, 16 | //quat_normalize, 17 | } from './lib/Maths.js'; 18 | 19 | import Translation from './mod/Translation.js'; 20 | import Rotation from './mod/Rotation.js'; 21 | import Faces from './mod/Faces.js'; 22 | 23 | import { Modes, Axes } from './Structs.js'; 24 | import { vec3_scale } from './lib/Maths.js'; 25 | 26 | export default function DragAction( ref, state, events ){ 27 | // #region MAIN 28 | const initPosition = [0,0,0]; 29 | const initRotation = [0,0,0,0]; 30 | const initHitPos = [0,0,0]; 31 | let axes = null; 32 | let axesLengths = null; 33 | let initMode = -1; 34 | let initAxis = -1; 35 | // #endregion 36 | 37 | // #region METHODS 38 | const start = ()=>{ 39 | initMode = state.selectMode; 40 | initAxis = state.selectAxis; 41 | axesLengths = state.axesLengths.slice(); 42 | axes = ref.getAxes(); 43 | vec3_copy( initPosition, state.position ); 44 | quat_copy( initRotation, state.rotation ); 45 | vec3_copy( initHitPos, state.rayHitPos ); 46 | 47 | state.isDragging = true; 48 | events.emit( 'dragStart' ); 49 | }; 50 | 51 | const stop = ()=>{ 52 | state.isDragging = false; 53 | events.emit( 'dragEnd' ); 54 | }; 55 | 56 | const move = ( ray )=>{ 57 | switch( initMode ){ 58 | case Modes.Translate:{ 59 | const pos = Translation.rayDrag( ray, initPosition, axes[ initAxis ], initHitPos ); 60 | if( pos ){ 61 | state.position = pos; 62 | return true; 63 | } 64 | break; } 65 | 66 | case Modes.Rotate:{ 67 | const rot = Rotation.rayDrag( ray, initPosition, axes, initAxis, initHitPos ); 68 | if( rot ){ 69 | state.rotation = quat_mul( [0,0,0,1], rot, initRotation ); 70 | return true; 71 | } 72 | break; } 73 | 74 | case Modes.Scale:{ 75 | const len = Faces.rayDrag( ray, initPosition, axes, initAxis, initHitPos, state.debug ); 76 | if( len ){ 77 | const al = axesLengths.slice(); 78 | al[ initAxis ] = len; 79 | state.axesLengths = al; 80 | return true; 81 | } 82 | break; } 83 | } 84 | 85 | return false; 86 | }; 87 | // #endregion 88 | 89 | return { start, stop, move }; 90 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3D Manipulator for ThreeJs 2 | [![twitter](https://img.shields.io/badge/Twitter-profile-blue?style=flat-square&logo=twitter)](https://twitter.com/SketchpunkLabs) 3 | [![npm](https://img.shields.io/badge/Github-donate-blue?style=flat-square&logo=github)](https://github.com/sponsors/sketchpunklabs) 4 | [![youtube](https://img.shields.io/badge/Youtube-subscribe-red?style=flat-square&logo=youtube)](https://youtube.com/c/sketchpunklabs) 5 | [![Patreon](https://img.shields.io/badge/Patreon-donate-red?style=flat-square&logo=youtube)](https://www.patreon.com/sketchpunk) 6 | [![Ko-Fi](https://img.shields.io/badge/Ko_Fi-donate-orange?style=flat-square&logo=youtube)](https://ko-fi.com/sketchpunk) 7 | 8 | 9 | 10 | https://www.npmjs.com/package/manipulator3d 11 | 12 | ### NPM Install ### 13 | ``` 14 | npm install manipulator3d 15 | ``` 16 | 17 | ### Development Setup ### 18 | ``` 19 | git clone --depth=1 https://github.com/sketchpunklabs/manipulator3d 20 | cd manipulator3d 21 | npm install 22 | npm run dev 23 | ``` 24 | 25 | ## Usage with attachment ### 26 | ```javascript 27 | import { Manipulator3D } from 'Manipulator3D'; 28 | 29 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | const cube = facedCube( null, 0.5 ); 31 | cube.position.set( 0, 1, 0 ); 32 | scene.add( cube ); 33 | 34 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | const man = new Manipulator3D( scene, camera, renderer ); 36 | 37 | // Hook into events to disable camera controller when user does a drag action 38 | man.on( 'dragstart', ()=>{ orbitControl.enabled = false; } ); 39 | man.on( 'dragend', ()=>{ orbitControl.enabled = true; } ); 40 | 41 | // Hook into events to see the changes happen 42 | // man.on( 'translate', e=>console.log( 'Translate', e.detail ) ); 43 | // man.on( 'rotate', e=>console.log( 'Rotate( Quat )', e.detail ) ); 44 | // man.on( 'scale', e=>console.log( 'Scale', e.detail ) ); 45 | 46 | // Turn on Control 47 | man.setActive( true ); 48 | 49 | // Attach object to manipulate 50 | man.attach( cube ); 51 | ``` 52 | 53 | ## Usage of translation without attachment ### 54 | ```javascript 55 | import { Manipulator3D } from 'Manipulator3D'; 56 | 57 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | const man = new Manipulator3D( scene, camera, renderer ); 59 | 60 | // Hook into events to disable camera controller when user does a drag action 61 | man.on( 'dragstart', ()=>{ orbitControl.enabled = false; } ); 62 | man.on( 'dragend', ()=>{ orbitControl.enabled = true; } ); 63 | 64 | // Hook into events to see the changes happen 65 | man.on( 'translate', e=>console.log( 'Translate', e.detail ) ); 66 | 67 | man.setActive( true ); // Turn on Control 68 | man.useRotate( false ); // Turn off Rotation 69 | man.useScale( false ); // Turn off Scale 70 | ``` -------------------------------------------------------------------------------- /src3/lib/MouseEvents.js: -------------------------------------------------------------------------------- 1 | import { Ray } from '../../src/RayIntersection.js'; 2 | 3 | 4 | export default function MouseEvents( camera, renderer, actions, state, autoLoad=false ){ 5 | // #region MAIN 6 | const oRay = new Ray(); 7 | const canvas = renderer.domElement; 8 | 9 | /** 10 | * Special case when there is an onClick Handler on the canvas, it 11 | * get triggered on mouse up, but if user did a drag action the click 12 | * will trigger at the end. This can cause issues if using click as a way 13 | * to select new attachments. 14 | */ 15 | let stopClick = false; 16 | // #endregion 17 | 18 | // #region EVENT HANDLERS 19 | const onClick = e =>{ if( stopClick ){ e.stopImmediatePropagation(); stopClick = false; } }; 20 | const onPointerUp = e =>{ if( actions.rayUp() ) canvas.releasePointerCapture( e.pointerId ); }; 21 | const onPointerMove = e =>{ 22 | updateRay( e ); 23 | 24 | if( !state.isDragging ){ 25 | actions.rayHover( oRay ); 26 | }else{ 27 | actions.rayMove( oRay ); 28 | canvas.setPointerCapture( e.pointerId ); // Keep receiving events 29 | e.preventDefault(); 30 | e.stopPropagation(); 31 | } 32 | }; 33 | 34 | const onPointerDown = e =>{ 35 | updateRay( e ); 36 | if( actions.rayDown( oRay ) ){ 37 | e.preventDefault(); 38 | e.stopPropagation(); 39 | stopClick = true; 40 | } 41 | }; 42 | // #endregion 43 | 44 | // #region HELPERS 45 | const updateRay = e =>{ 46 | const rect = canvas.getBoundingClientRect(); 47 | const x = e.clientX - rect.x; // canvas x position 48 | const y = e.clientY - rect.y; // canvas y position 49 | const camProj = camera.projectionMatrix.toArray(); // Need Projection Matrix 50 | const camWorld = camera.matrixWorld.toArray(); // World Space Transform of Camera 51 | oRay.fromScreenProjection( x, y, rect.width, rect.height, camProj, camWorld ); 52 | }; 53 | // #endregion 54 | 55 | // #region METHODS 56 | const loadListeners = ()=>{ 57 | canvas.addEventListener( 'click', onClick ); 58 | canvas.addEventListener( 'pointermove', onPointerMove ); 59 | canvas.addEventListener( 'pointerdown', onPointerDown ); 60 | canvas.addEventListener( 'pointerup', onPointerUp ); 61 | return this; 62 | }; 63 | 64 | const unloadListeners = ()=>{ 65 | canvas.removeEventListener( 'click', onClick ); 66 | canvas.removeEventListener( 'pointermove', onPointerMove ); 67 | canvas.removeEventListener( 'pointerdown', onPointerDown ); 68 | canvas.removeEventListener( 'pointerup', onPointerUp ); 69 | return this; 70 | }; 71 | // #endregion 72 | 73 | if( autoLoad ) loadListeners(); 74 | return { loadListeners, unloadListeners }; 75 | } -------------------------------------------------------------------------------- /annotatebox/lib/MouseEvents.js: -------------------------------------------------------------------------------- 1 | import { Ray } from '../../src/RayIntersection.js'; 2 | 3 | 4 | export default function MouseEvents( camera, renderer, actions, state, autoLoad=false ){ 5 | // #region MAIN 6 | const oRay = new Ray(); 7 | const canvas = renderer.domElement; 8 | 9 | /** 10 | * Special case when there is an onClick Handler on the canvas, it 11 | * get triggered on mouse up, but if user did a drag action the click 12 | * will trigger at the end. This can cause issues if using click as a way 13 | * to select new attachments. 14 | */ 15 | let stopClick = false; 16 | // #endregion 17 | 18 | // #region EVENT HANDLERS 19 | const onClick = e =>{ if( stopClick ){ e.stopImmediatePropagation(); stopClick = false; } }; 20 | const onPointerUp = e =>{ if( actions.rayUp() ) canvas.releasePointerCapture( e.pointerId ); }; 21 | const onPointerMove = e =>{ 22 | updateRay( e ); 23 | 24 | if( !state.isDragging ){ 25 | actions.rayHover( oRay ); 26 | }else{ 27 | actions.rayMove( oRay ); 28 | canvas.setPointerCapture( e.pointerId ); // Keep receiving events 29 | e.preventDefault(); 30 | e.stopPropagation(); 31 | } 32 | }; 33 | 34 | const onPointerDown = e =>{ 35 | updateRay( e ); 36 | if( actions.rayDown( oRay ) ){ 37 | e.preventDefault(); 38 | e.stopPropagation(); 39 | stopClick = true; 40 | } 41 | }; 42 | // #endregion 43 | 44 | // #region HELPERS 45 | const updateRay = e =>{ 46 | const rect = canvas.getBoundingClientRect(); 47 | const x = e.clientX - rect.x; // canvas x position 48 | const y = e.clientY - rect.y; // canvas y position 49 | const camProj = camera.projectionMatrix.toArray(); // Need Projection Matrix 50 | const camWorld = camera.matrixWorld.toArray(); // World Space Transform of Camera 51 | oRay.fromScreenProjection( x, y, rect.width, rect.height, camProj, camWorld ); 52 | }; 53 | // #endregion 54 | 55 | // #region METHODS 56 | const loadListeners = ()=>{ 57 | canvas.addEventListener( 'click', onClick ); 58 | canvas.addEventListener( 'pointermove', onPointerMove ); 59 | canvas.addEventListener( 'pointerdown', onPointerDown ); 60 | canvas.addEventListener( 'pointerup', onPointerUp ); 61 | return this; 62 | }; 63 | 64 | const unloadListeners = ()=>{ 65 | canvas.removeEventListener( 'click', onClick ); 66 | canvas.removeEventListener( 'pointermove', onPointerMove ); 67 | canvas.removeEventListener( 'pointerdown', onPointerDown ); 68 | canvas.removeEventListener( 'pointerup', onPointerUp ); 69 | return this; 70 | }; 71 | // #endregion 72 | 73 | if( autoLoad ) loadListeners(); 74 | return { loadListeners, unloadListeners }; 75 | } -------------------------------------------------------------------------------- /annotatebox/mod/Rotation.js: -------------------------------------------------------------------------------- 1 | import { vec3_add, vec3_angle, vec3_norm } from '../lib/Maths.js'; 2 | import { 3 | // Ray, 4 | intersectPlane, 5 | // intersectTri, 6 | // intersectSphere, 7 | // nearPoint, 8 | //NearSegmentResult, 9 | //nearSegment, 10 | } from '../lib/RayIntersection.js'; 11 | 12 | 13 | import { 14 | vec3_sub, 15 | vec3_len, 16 | vec3_dot, 17 | vec3_scaleAndAdd, 18 | quat_rotateTo, 19 | } from '../lib/Maths.js'; 20 | import { ZeroCurvatureEnding } from 'three'; 21 | import { Axes } from '../Structs.js'; 22 | 23 | export default class Rotation{ 24 | static minRayDistance = 0.1; 25 | static minAngle = 11 * Math.PI / 180; 26 | 27 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 28 | //const minRange = this.minRayDistance * state.cameraScale; 29 | const origin = state.position; 30 | const hitDir = [0,0,0]; 31 | const toHit = [0,0,0]; 32 | let t; 33 | let ii; 34 | let dist; 35 | let radius; 36 | 37 | // let debug = state.debug; 38 | // debug.pnt.reset(); 39 | // debug.ln.reset(); 40 | 41 | for( let i=0; i < 3; i++ ){ 42 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | // First test against the plane using the axis as the plane normal 44 | t = intersectPlane( ray, origin, axes[i] ); 45 | if( t === null ) continue; 46 | 47 | ii = (i + 1) % 3; // Need the next axis over to perform tests 48 | 49 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | // Next do a circle radius test of the hit point to plane origin 51 | ray.posAt( t, hitPos ); 52 | radius = state.axesLengths[ ii ] + state.axisLen; 53 | dist = vec3_len( hitPos, origin ); 54 | 55 | //debug.pnt.add( vec3_scaleAndAdd( [0,0,0], origin, axes[ii], radius ), 0xffff00, 13 ); 56 | //debug.pnt.add( hitPos, 0x00ff00, 7 ); 57 | 58 | 59 | if( Math.abs( radius - dist ) <= this.minRayDistance ){ 60 | //debug.pnt.add( hitPos, 0xffffff, 13 ); 61 | // Direction to hit position 62 | vec3_sub( toHit, hitPos, origin ); 63 | vec3_norm( toHit, toHit ); 64 | 65 | if( vec3_angle( axes[ii], toHit ) <= this.minAngle ){ 66 | 67 | //debug.ln.add( origin, vec3_scaleAndAdd( [0,0,0], origin, toHit, 4 ), 0xffffff ); 68 | //debug.ln.add( origin, vec3_scaleAndAdd( [0,0,0], origin, axes[ii], 4 ), 0xffff00 ); 69 | 70 | //console.log( i, vec3_angle( axes[ii], toHit ) * 180 / Math.PI ); 71 | 72 | return i; 73 | 74 | } 75 | } 76 | 77 | } 78 | 79 | return -1; 80 | } 81 | 82 | static rayDrag( ray, origin, axes, iAxis, hitPos ){ 83 | let t = intersectPlane( ray, origin, axes[ iAxis ] ); 84 | if( t === null ) return null; 85 | 86 | const fromDir = vec3_sub( [0,0,0], hitPos, origin ); 87 | vec3_norm( fromDir, fromDir ); 88 | 89 | const toDir = vec3_sub( [0,0,0], ray.posAt( t ), origin ); 90 | vec3_norm( toDir, toDir ); 91 | 92 | return quat_rotateTo( [0,0,0,1], fromDir, toDir ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src2/Manipulator3D.js: -------------------------------------------------------------------------------- 1 | import ManipulatorState from './state/ManipulatorState.js'; 2 | import ManipulatorActions from './ManipulatorActions.js'; 3 | import MouseEvents from './MouseEvents.js'; 4 | import Tracer from './Tracer.js'; 5 | 6 | 7 | export default class Manipulator3D{ 8 | // #region MAIN 9 | state = new ManipulatorState(); 10 | actions = new ManipulatorActions( this ); 11 | trace = new Tracer(); 12 | camera = null; 13 | mouseEvents = null; 14 | 15 | constructor( camera, renderer ){ 16 | this.camera = camera; 17 | this.mouseEvents = new MouseEvents( this, renderer ); 18 | } 19 | // #endregion 20 | 21 | // #region DRAGGING 22 | startDrag(){ 23 | this.state.isDragging = true; 24 | this.trace.prepare( this ); 25 | this.state.doCache(); 26 | 27 | 28 | //console.log( 'startDrag' ); 29 | 30 | // // Save initial state of attached object 31 | // if( this.attachedObject ){ 32 | // vec3_copy( this._initDragPosition, this.attachedObject.position.toArray() ); 33 | // vec3_copy( this._initDragScale, this.attachedObject.scale.toArray() ); 34 | // quat_copy( this._initDragQuaternion, this.attachedObject.quaternion.toArray() ); 35 | // }else{ 36 | // // Continue using 'current' values in no attachment use of the control. 37 | // vec3_copy( this._initDragPosition, this._currentPosition ); 38 | // vec3_copy( this._initDragScale, this._currentScale ); 39 | // quat_copy( this._initDragQuaternion, this._currentQuaternion ); 40 | // } 41 | 42 | // // Offset has prevent the snapping effect of translation 43 | // vec3_sub( this._intersectOffset, this.data.position, this.data.intersectPos ); 44 | 45 | // // When dealing with small objects, better to hide the gizmo during scale & rotation 46 | // // But leave the trace line visible as its really the only ui the user needs during dragging 47 | // if( this.data.activeMode !== ManipulatorMode.Translate ){ 48 | // this.mesh.hideGizmo(); 49 | // } 50 | 51 | // this.data.hasUpdated = true; // Need mesh to update 52 | 53 | this.emit( 'dragstart' ); 54 | } 55 | 56 | endDrag(){ 57 | this.state.isDragging = false; 58 | //console.log( 'endDrag' ); 59 | 60 | // // onTranslate updates position, need to recalculate at the end of dragging 61 | // // for intersection tests to be accurate. 62 | // this.data.calcAxesPosition(); 63 | 64 | // // When doing dragging away from ui, the hover event won't trigger to undo 65 | // // visual states, so call method at the end of the dragging to tidy things up. 66 | // this.data.resetState(); 67 | // this.mesh.showGizmo(); 68 | 69 | this.emit( 'dragend' ); 70 | } 71 | // #endregion 72 | 73 | // #region OUTER EVENTS{ 74 | on( evtName, fn ){ document.body.addEventListener( evtName, fn ); return this; } 75 | off( evtName, fn ){ document.body.removeEventListener( evtName, fn ); return this; } 76 | emit( evtName, detail=null ){ 77 | document.body.dispatchEvent( new CustomEvent( evtName, { detail, bubbles:true, cancelable:true, composed:false } ) ); 78 | } 79 | // #endregion 80 | } -------------------------------------------------------------------------------- /src2/MouseEvents.js: -------------------------------------------------------------------------------- 1 | import { 2 | Ray, 3 | // intersectPlane, 4 | // intersectTri, 5 | // intersectSphere, 6 | // nearPoint, 7 | // NearSegmentResult, 8 | // nearSegment, 9 | } from '../src/RayIntersection.js'; 10 | 11 | export default class MouseEvents{ 12 | // #region MAIN 13 | constructor( man, renderer ){ 14 | this.ray = new Ray(); 15 | this.canvas = renderer.domElement; 16 | this.man = man; 17 | this.loadListeners(); 18 | 19 | /** 20 | * Special case when there is an onClick Handler on the canvas, it 21 | * get triggered on mouse up, but if user did a drag action the click 22 | * will trigger at the end. This can cause issues if using click as a way 23 | * to select new attachments. 24 | */ 25 | this.stopClick = false; 26 | } 27 | // #endregion 28 | 29 | // #region METHODS 30 | loadListeners(){ 31 | this.canvas.addEventListener( 'click', this.onClick ); 32 | this.canvas.addEventListener( 'pointermove', this.onPointerMove ); 33 | this.canvas.addEventListener( 'pointerdown', this.onPointerDown ); 34 | this.canvas.addEventListener( 'pointerup', this.onPointerUp ); 35 | return this; 36 | } 37 | 38 | unloadListeners(){ 39 | this.canvas.removeEventListener( 'click', this.onClick ); 40 | this.canvas.removeEventListener( 'pointermove', this.onPointerMove ); 41 | this.canvas.removeEventListener( 'pointerdown', this.onPointerDown ); 42 | this.canvas.removeEventListener( 'pointerup', this.onPointerUp ); 43 | } 44 | 45 | updateRay( e ){ 46 | const rect = this.canvas.getBoundingClientRect(); 47 | const x = e.clientX - rect.x; // canvas x position 48 | const y = e.clientY - rect.y; // canvas y position 49 | const camProj = this.man.camera.projectionMatrix.toArray(); // Need Projection Matrix 50 | const camWorld = this.man.camera.matrixWorld.toArray(); // World Space Transform of Camera 51 | this.ray.fromScreenProjection( x, y, rect.width, rect.height, camProj, camWorld ); 52 | } 53 | // #endregion 54 | 55 | // #region HANDLERS 56 | onClick = ( e )=>{ 57 | if( this.stopClick ){ 58 | e.stopImmediatePropagation(); 59 | this.stopClick = false; 60 | } 61 | }; 62 | 63 | onPointerMove = ( e )=>{ 64 | this.updateRay( e ); 65 | 66 | if( !this.man.state.isDragging ){ 67 | this.man.actions.rayHover( this.ray ); 68 | }else{ 69 | this.man.actions.rayMove( this.ray ); 70 | this.canvas.setPointerCapture( e.pointerId ); // Keep receiving events 71 | e.preventDefault(); 72 | e.stopPropagation(); 73 | } 74 | }; 75 | 76 | onPointerDown = ( e )=>{ 77 | this.updateRay( e ); 78 | 79 | if( this.man.actions.rayDown( this.ray ) ){ 80 | e.preventDefault(); 81 | e.stopPropagation(); 82 | this.stopClick = true; 83 | } 84 | }; 85 | 86 | onPointerUp = ( e )=>{ 87 | if( this.man.actions.rayUp() ){ 88 | this.canvas.releasePointerCapture( e.pointerId ); 89 | } 90 | }; 91 | // #endregion 92 | } 93 | -------------------------------------------------------------------------------- /test/_lib/DynamicMesh.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | class DynamicMesh{ 4 | mesh = null; 5 | 6 | vertices = []; 7 | indices = []; 8 | normals = []; 9 | 10 | _vertCount = 0; 11 | _normCount = 0; 12 | _idxCount = 0; 13 | 14 | constructor( mat=null ){ 15 | this.mesh = new THREE.Mesh(); 16 | this.mesh.material = mat || new THREE.MeshPhongMaterial( {color:0x00ffff, side:THREE.DoubleSide } ); 17 | } 18 | 19 | rebuild( nGeo=null ){ 20 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | if( nGeo ){ 22 | if( nGeo.vertices ) this.vertices = nGeo.vertices; 23 | if( nGeo.indices ) this.indices = nGeo.indices; 24 | if( nGeo.normals ) this.normals = nGeo.normals; 25 | } 26 | 27 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | // Geometry Buffers can't not be resized in ThreeJS 29 | // Note : Buffers themselves in WebGL can, just a limitation of the Framework. 30 | // Because of this, will need to recreate the Geometry Object if size is larger. 31 | if( this.vertices.length > this._vertCount || 32 | this.indices.length > this._idxCount || 33 | this.normals.length > this._normCount ){ 34 | 35 | this.mesh.geometry.dispose(); 36 | this.mesh.geometry = null; 37 | this._mkGeo(); 38 | 39 | return this; 40 | } 41 | 42 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | let geo = this.mesh.geometry; 44 | 45 | geo.attributes.position.array.set( this.vertices ); 46 | geo.attributes.position.needsUpdate = true; 47 | 48 | if( this.normals.length > 0 ){ 49 | geo.attributes.normal.array.set( this.normals ); 50 | geo.attributes.normal.needsUpdate = true; 51 | } 52 | 53 | if( this.indices.length > 0 ){ 54 | geo.index.array.set( this.indices ); 55 | geo.index.needsUpdate = true; 56 | geo.setDrawRange( 0, this.indices.length ); 57 | }else geo.setDrawRange( 0, this.vertices.length / 3 ); 58 | 59 | geo.computeBoundingBox(); 60 | geo.computeBoundingSphere(); 61 | 62 | return this; 63 | } 64 | 65 | reset(){ 66 | this.vertices.length = 0; 67 | this.indices.length = 0; 68 | this.normals.length = 0; 69 | return this; 70 | } 71 | 72 | _mkGeo(){ 73 | //---------------------------------- 74 | // Define Geometry Object 75 | const bGeo = new THREE.BufferGeometry(); 76 | bGeo.setAttribute( "position", new THREE.BufferAttribute( new Float32Array( this.vertices ), 3 ) ); 77 | 78 | if( this.normals.length > 0 ) bGeo.setAttribute( "normal", new THREE.BufferAttribute( new Float32Array( this.normals ), 3 ) ); 79 | if( this.indices.length > 0 ){ 80 | // ThreeJS doesn't seem to like type arrays when setting indices, If its a 81 | // typeArray, convert it to a javascript array. 82 | // TODO: Look up how to setup indices using Buffers instead. 83 | if( this.indices.byteLength ) bGeo.setIndex( Array.from( this.indices ) ); 84 | else bGeo.setIndex( this.indices ); 85 | } 86 | 87 | this.mesh.geometry = bGeo; 88 | 89 | //---------------------------------- 90 | if( this.vertices.length > this._vertCount ) this._vertCount = this.vertices.length; 91 | if( this.indices.length > this._idxCount ) this._idxCount = this.indices.length; 92 | if( this.normals.length > this._normCount ) this._normCount = this.normals.length; 93 | } 94 | } 95 | 96 | export default DynamicMesh; -------------------------------------------------------------------------------- /annotatebox/Actions.js: -------------------------------------------------------------------------------- 1 | // #region IMPORTS 2 | // import Translation from './mod/Translation.js'; 3 | // import Rotation from './mod/Rotation.js'; 4 | // import Scale from './mod/Scale.js'; 5 | import Faces from './mod/Faces.js'; 6 | import Translation from './mod/Translation.js'; 7 | import Rotation from './mod/Rotation.js'; 8 | 9 | // import DragAction from './DragAction.js'; 10 | import { Modes, Axes } from './Structs.js'; 11 | 12 | import { intersectQuad } from './lib/RayIntersection.js'; 13 | 14 | import DragAction from './DragAction.js'; 15 | 16 | import { 17 | vec3_add_batch, 18 | // vec3_add, 19 | // vec3_sub, 20 | // vec3_len, 21 | // vec3_norm, 22 | // vec3_scale, 23 | vec3_transformQuat, 24 | vec3_sqrLen, 25 | // vec3_copy, 26 | // quat_mul, 27 | // quat_copy, 28 | // quat_setAxisAngle, 29 | // quat_normalize, 30 | } from './lib/Maths.js'; 31 | // #endregion 32 | 33 | export default function Actions( ref, state, events ){ 34 | // #region MAIN 35 | const dragging = DragAction( ref, state, events ); 36 | let pnts = [ 37 | [0,0,0], [0,0,0], [0,0,0], [0,0,0], // Bottom : Start back left, front left, front right, back right 38 | [0,0,0], [0,0,0], [0,0,0], [0,0,0], // Top 39 | ]; 40 | // #endregion 41 | 42 | // #region HELPERS 43 | const rayIntersect = ray =>{ 44 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | // Get the current state axis from rotation 46 | const axes = ref.getAxes(); 47 | const hit = [Infinity,Infinity,Infinity]; 48 | 49 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | const rotAxis = Rotation.rayTest( ray, state, axes, hit ); 51 | //const rotDist = ( rotAxis === -1 )? Infinity : vec3_sqrLen( ray.posStart, rotHit ); 52 | if( rotAxis !== -1 ){ 53 | state.selectAxis = rotAxis; 54 | state.selectMode = Modes.Rotate; 55 | state.rayHitPos = hit; 56 | return true; 57 | } 58 | 59 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 60 | const posAxis = Translation.rayTest( ray, state, axes, hit ); 61 | if( posAxis !== -1 ){ 62 | state.selectAxis = posAxis; 63 | state.selectMode = Modes.Translate; 64 | state.rayHitPos = hit; 65 | return true; 66 | } 67 | 68 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | const faceAxis = Faces.rayTest( ray, state, axes, hit ); 70 | if( faceAxis !== -1 ){ 71 | state.selectAxis = faceAxis; 72 | state.selectMode = Modes.Scale; 73 | state.rayHitPos = hit; 74 | return true; 75 | } 76 | 77 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 78 | state.selectAxis = Axes.None; 79 | state.selectMode = Modes.Translate; 80 | return false; 81 | }; 82 | // #endregion 83 | 84 | // #region EXPORT 85 | return { 86 | rayDown( ray ){ 87 | if( state.isActive && rayIntersect( ray ) ){ 88 | dragging.start(); 89 | return true; 90 | } 91 | return false; 92 | }, 93 | 94 | rayUp( ray ){ 95 | if( state.isDragging ) dragging.stop (); 96 | }, 97 | 98 | rayMove( ray ){ 99 | if( !state.isActive || !state.isDragging ) return false; 100 | return dragging.move( ray ); 101 | }, 102 | 103 | rayHover( ray ){ 104 | return ( state.isActive && !state.isDragging )? rayIntersect( ray ) : false; 105 | }, 106 | }; 107 | // #endregion 108 | } 109 | -------------------------------------------------------------------------------- /src3/render/FloorCube.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | // Create a cube with its origin at the bottom left back corner 4 | export default class FloorCube{ 5 | static mesh( mat=null, pos=null, scl=null ){ 6 | const bGeo = this.geo(); 7 | const mesh = new THREE.Mesh( bGeo, mat || new THREE.MeshPhongMaterial( { color:0x009999 } ) ); 8 | if( pos ) mesh.position.fromArray( pos ); 9 | if( scl != null ) mesh.scale.set( scl, scl, scl ); 10 | 11 | return mesh; 12 | } 13 | 14 | static geo( width=1, height=1, depth=1 ){ 15 | const geo = this.get( width, height, depth ); 16 | const bGeo = new THREE.BufferGeometry(); 17 | bGeo.setIndex( new THREE.BufferAttribute( geo.indices, 1 ) ); 18 | bGeo.setAttribute( 'position', new THREE.BufferAttribute( geo.vertices, 3 ) ); 19 | bGeo.setAttribute( 'normal', new THREE.BufferAttribute( geo.normals, 3 ) ); 20 | bGeo.setAttribute( 'uv', new THREE.BufferAttribute( geo.texcoord, 2 ) ); 21 | return bGeo; 22 | } 23 | 24 | static get( width=1, height=1, depth=1 ){ 25 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | const x1 = width * 0.5, 27 | y1 = height, // * 0.5, 28 | z1 = depth * 0.5, 29 | x0 = -x1, 30 | y0 = 0, //-y1, 31 | z0 = -z1; 32 | 33 | // Starting bottom left corner, then working counter clockwise to create the front face. 34 | // Backface is the first face but in reverse (3,2,1,0) 35 | // keep each quad face built the same way to make index and uv easier to assign 36 | const vert = [ 37 | x0, y1, z1, //0 Front 38 | x0, y0, z1, //1 39 | x1, y0, z1, //2 40 | x1, y1, z1, //3 41 | 42 | x1, y1, z0, //4 Back 43 | x1, y0, z0, //5 44 | x0, y0, z0, //6 45 | x0, y1, z0, //7 46 | 47 | x1, y1, z1, //3 Right 48 | x1, y0, z1, //2 49 | x1, y0, z0, //5 50 | x1, y1, z0, //4 51 | 52 | x0, y0, z1, //1 Bottom 53 | x0, y0, z0, //6 54 | x1, y0, z0, //5 55 | x1, y0, z1, //2 56 | 57 | x0, y1, z0, //7 Left 58 | x0, y0, z0, //6 59 | x0, y0, z1, //1 60 | x0, y1, z1, //0 61 | 62 | x0, y1, z0, //7 Top 63 | x0, y1, z1, //0 64 | x1, y1, z1, //3 65 | x1, y1, z0, //4 66 | ]; 67 | 68 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | //Build the index of each quad [0,1,2, 2,3,0] 70 | let i; 71 | const idx = []; 72 | for( i=0; i < vert.length / 3; i+=2) idx.push( i, i+1, ( Math.floor( i / 4 ) * 4 ) + ( ( i + 2 ) % 4 ) ); 73 | 74 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 75 | //Build UV data for each vertex 76 | const uv = []; 77 | for( i=0; i < 6; i++) uv.push( 0,0, 0,1, 1,1, 1,0 ); 78 | 79 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | 81 | return { 82 | vertices : new Float32Array( vert ), 83 | indices : new Uint16Array( idx ), 84 | texcoord : new Float32Array( uv ), 85 | normals : new Float32Array( [ // Left/Right have their xNormal flipped to render correctly in 3JS, Why does normals need to be mirrored on X? 86 | 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, //Front 87 | 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, //Back 88 | 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, //Left 89 | 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, //Bottom 90 | -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, //Right 91 | 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0 //Top 92 | ] ), 93 | }; 94 | } 95 | } -------------------------------------------------------------------------------- /src2/state/ManipulatorState.js: -------------------------------------------------------------------------------- 1 | import PositionState from './PositionState.js'; 2 | import RotationState from './RotationState.js'; 3 | import ScaleState from './ScaleState.js'; 4 | 5 | 6 | import { 7 | // vec3_add, 8 | vec3_sub, 9 | vec3_len, 10 | vec3_copy, 11 | // vec3_transformQuat, 12 | vec3_norm, 13 | vec3_dot, 14 | vec3_scale, 15 | // vec3_scaleAndAdd, 16 | vec3_sqrLen, 17 | // quat_mul, 18 | // quat_normalize, 19 | quat_copy, 20 | quat_sqrLen, 21 | // quat_setAxisAngle, 22 | } from '../../src/Maths.js'; 23 | 24 | 25 | import ManipulatorMode from '../ManipulatorMode.js'; 26 | 27 | export default class ManipulatorState{ 28 | // #region MAIN 29 | // Main Transformation State 30 | position = new PositionState(); 31 | rotation = new RotationState(); 32 | scale = new ScaleState(); 33 | 34 | // Scale data based on camera distance 35 | cameraRot = [0,0,0,1]; 36 | cameraPos = [0,0,0]; 37 | cameraFactor = 10; 38 | cameraScale = 1; 39 | cameraVecScale = [1,1,1]; 40 | cameraFlipDMin = -0.02; 41 | 42 | // settings 43 | isActive = true; 44 | isDragging = false; 45 | rotateStep = 2 * Math.PI / 180; // How many radians per step 46 | 47 | // misc 48 | radius = 1.5; // Distance each axis should stick out from origin 49 | mode = ManipulatorMode.None; 50 | iAxis = -1; 51 | contactPos = [0,0,0]; 52 | 53 | // cache 54 | _initRotation = [0,0,0,0]; 55 | _initAxis = [0,0,0]; 56 | 57 | constructor(){} 58 | // #endregion 59 | 60 | // #region CAMERA 61 | updateFromCamera( pos, rot ){ 62 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | // Only update if the camera has changed since last update 64 | if( 65 | Math.abs( vec3_sqrLen( pos, this.cameraPos ) ) <= 0.000001 && 66 | Math.abs( quat_sqrLen( rot, this.cameraRot ) ) <= 0.000001 67 | ) return; 68 | 69 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | vec3_copy( this.cameraPos, pos ); // Save camera state 71 | quat_copy( this.cameraRot, rot ); 72 | this.updateCameraScale(); // Update cameral scaling 73 | this.updateData(); // Update other bits of data 74 | return this; 75 | } 76 | 77 | updateCameraScale(){ 78 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | // Adjust the scale to keep the gizmo as the same size no matter how far the camera goes 80 | const eyeDir = vec3_sub( [0,0,0], this.cameraPos, this.position.value ); 81 | const eyeLen = vec3_len( eyeDir ); 82 | this.cameraScale = eyeLen / this.cameraFactor; 83 | 84 | vec3_norm( eyeDir, eyeDir ); // Normalize for DOT Checks 85 | vec3_scale( this.cameraVecScale, [1,1,1], this.cameraScale ); 86 | 87 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 88 | // Flip viewing to the opposite side 89 | if( vec3_dot( eyeDir, [1,0,0] ) < this.cameraFlipDMin ) this.cameraVecScale[0] = -this.cameraVecScale[0]; 90 | if( vec3_dot( eyeDir, [0,1,0] ) < this.cameraFlipDMin ) this.cameraVecScale[1] = -this.cameraVecScale[1]; 91 | if( vec3_dot( eyeDir, [0,0,1] ) < this.cameraFlipDMin ) this.cameraVecScale[2] = -this.cameraVecScale[2]; 92 | } 93 | // #endregion 94 | 95 | // #region METHODS 96 | updateData(){ 97 | this.rotation.updateScale( this ); // Update scaled axis 98 | this.position.updateEndpoints( this ); // Update axis end points 99 | } 100 | 101 | doCache(){ 102 | quat_copy( this._initRotation, this.rotation.value ); 103 | vec3_copy( this._initAxis, this.rotation.aryAxis[ this.iAxis ] ); 104 | } 105 | // #endregion 106 | } -------------------------------------------------------------------------------- /test/ManipulatorDebugger.js: -------------------------------------------------------------------------------- 1 | import { Group } from 'three'; 2 | import DynLineMesh from './_lib/DynLineMesh.js'; 3 | import ShapePointsMesh from './_lib/ShapePointsMesh.js'; 4 | 5 | export default class ManipulatorDebugger extends Group{ 6 | meshAxis = new DynLineMesh(); 7 | meshPoints = new ShapePointsMesh(); 8 | 9 | constructor(){ 10 | super(); 11 | this.add( this.meshAxis ); 12 | this.add( this.meshPoints ); 13 | } 14 | 15 | update( data, forceUpdate=false ){ 16 | if( !data.hasUpdated && !data.hasHit && !forceUpdate ) return; 17 | 18 | const a = [0,0,0]; 19 | const b = [0,0,0]; 20 | const pntSize = 6; 21 | const pntShape = 2; 22 | this.meshAxis.reset(); 23 | this.meshPoints.reset(); 24 | 25 | // Axes Lines 26 | this.meshAxis.add( data.position, data.axes[0].endPos, 0xffff00 ); 27 | this.meshAxis.add( data.position, data.axes[1].endPos, 0x00ffff ); 28 | this.meshAxis.add( data.position, data.axes[2].endPos, 0x00ff00 ); 29 | 30 | // Mind Point lines ( Planes ) 31 | this.meshAxis.add( data.axes[0].midPos, data.axes[1].midPos, 0xffff00, 0x00ffff ); 32 | this.meshAxis.add( data.axes[1].midPos, data.axes[2].midPos, 0x00ffff, 0x00ff00 ); 33 | this.meshAxis.add( data.axes[2].midPos, data.axes[0].midPos, 0x00ff00, 0xffff00 ); 34 | 35 | // Draw End Points 36 | this.meshPoints.add( data.axes[0].endPos, 0xffff00, pntSize * Math.abs( data.scale[0] ), pntShape ); 37 | this.meshPoints.add( data.axes[1].endPos, 0x00ffff, pntSize * Math.abs( data.scale[1] ), pntShape ); 38 | this.meshPoints.add( data.axes[2].endPos, 0x00ff00, pntSize * Math.abs( data.scale[2] ), pntShape ); 39 | 40 | // Draw Mid Points 41 | this.meshPoints.add( data.axes[0].midPos, 0xffff00, 3 * Math.abs( data.scale[0] ), 1 ); 42 | this.meshPoints.add( data.axes[1].midPos, 0x00ffff, 3 * Math.abs( data.scale[1] ), 1 ); 43 | this.meshPoints.add( data.axes[2].midPos, 0x00ff00, 3 * Math.abs( data.scale[2] ), 1 ); 44 | 45 | // Draw Scl Points 46 | this.meshPoints.add( data.axes[0].sclPos, 0xffff00, 5 * Math.abs( data.scale[0] ), 0 ); 47 | this.meshPoints.add( data.axes[1].sclPos, 0x00ffff, 5 * Math.abs( data.scale[1] ), 0 ); 48 | this.meshPoints.add( data.axes[2].sclPos, 0x00ff00, 5 * Math.abs( data.scale[2] ), 0 ); 49 | 50 | // Draw axes guides 51 | // let ax; 52 | // for( let i=0; i < 3; i++ ){ 53 | // ax = data.axes[ i ]; 54 | // if( ax.isActive ){ 55 | // this.meshAxis.add( ax.tracePoints.a, ax.tracePoints.b, 0x909090 ); 56 | // } 57 | // } 58 | 59 | // TraceLine 60 | if( data.traceLine.isActive ){ 61 | this.meshAxis.add( data.traceLine.a, data.traceLine.b, 0x909090 ); 62 | //this.meshPoints.add( data.hitPos, 0xffffff, 5 * Math.abs( data.scale[2] ), 7 ); 63 | this.meshPoints.add( data.traceLine.origin, 0xffffff, 5 * Math.abs( data.scale[2] ), 7 ); 64 | //console.log( 'x', data.traceLine.hit ); 65 | this.meshPoints.add( data.traceLine.hitPos, 0xffffff, 5 * Math.abs( data.scale[2] ), 1 ); 66 | ///Debug.pnt.add( data.traceLine.hit, 0xffffff, 10 ); 67 | } 68 | 69 | // Draw Rotation Arcs 70 | const xSign = Math.sign( data.scale[0] ); 71 | const ySign = Math.sign( data.scale[1] ); 72 | const zSign = Math.sign( data.scale[2] ); 73 | 74 | this.meshAxis.arc( data.position, data.axes[0].dir, data.axes[2].dir, data.scale[0] * data.arcRadius, 6, Math.PI*0.5*xSign*zSign, 0, 0x00ffff ); 75 | this.meshAxis.arc( data.position, data.axes[1].dir, data.axes[2].dir, data.scale[1] * data.arcRadius, 6, Math.PI*0.5*zSign*ySign, 0, 0xffff00 ); 76 | this.meshAxis.arc( data.position, data.axes[1].dir, data.axes[0].dir, data.scale[1] * data.arcRadius, 6, Math.PI*0.5*xSign*ySign, 0, 0x00ff00 ); 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src3/DragAction.js: -------------------------------------------------------------------------------- 1 | import { 2 | // vec3_add, 3 | // vec3_sub, 4 | // vec3_len, 5 | // vec3_norm, 6 | // vec3_scale, 7 | vec3_inv_scale, 8 | vec3_transformQuat, 9 | // vec3_sqrLen, 10 | vec3_copy, 11 | quat_mul, 12 | quat_copy, 13 | vec3_add, 14 | vec3_scaleAndAdd, 15 | //quat_setAxisAngle, 16 | //quat_normalize, 17 | } from './lib/Maths.js'; 18 | 19 | import Translation from './mod/Translation.js'; 20 | import Rotation from './mod/Rotation.js'; 21 | import Plane from './mod/Plane.js'; 22 | import Scale from './mod/Scale.js'; 23 | 24 | import { Modes, Axes } from './Structs.js'; 25 | import { vec3_scale } from '../src/Maths.js'; 26 | 27 | export default function DragAction( state, events ){ 28 | // #region MAIN 29 | const initPosition = [0,0,0]; 30 | const initRotation = [0,0,0,0]; 31 | const initScale = [0,0,0]; 32 | const initHitPos = [0,0,0]; 33 | const axes = [ 34 | [1,0,0], 35 | [0,1,0], 36 | [0,0,1], 37 | ]; 38 | 39 | let initMode = -1; 40 | let initAxis = -1; 41 | // #endregion 42 | 43 | // #region METHODS 44 | const start = ()=>{ 45 | if( state.target ){ 46 | vec3_copy( initPosition, state.target.position.toArray() ); 47 | vec3_copy( initScale, state.target.scale.toArray() ); 48 | quat_copy( initRotation, state.target.quaternion.toArray() ); 49 | }else{ 50 | vec3_copy( initPosition, state.position ); 51 | vec3_copy( initScale, state.scale ); 52 | quat_copy( initRotation, state.rotation ); 53 | } 54 | 55 | vec3_copy( initHitPos, state.rayHitPos ); 56 | vec3_transformQuat( axes[0], [1,0,0], initRotation ); 57 | vec3_transformQuat( axes[1], [0,1,0], initRotation ); 58 | vec3_transformQuat( axes[2], [0,0,1], initRotation ); 59 | 60 | initMode = state.selectMode; 61 | initAxis = state.selectAxis; 62 | 63 | state.isDragging = true; 64 | events.emit( 'dragStart' ); 65 | }; 66 | 67 | const stop = ()=>{ 68 | state.isDragging = false; 69 | events.emit( 'dragEnd' ); 70 | }; 71 | 72 | const move = ( ray )=>{ 73 | switch( initMode ){ 74 | case Modes.Translate:{ 75 | const pos = Translation.rayDrag( ray, initPosition, axes[ initAxis ], initHitPos ); 76 | if( pos ){ 77 | state.position = pos; 78 | return true; 79 | } 80 | break; } 81 | 82 | case Modes.Plane:{ 83 | const pos = Plane.rayDrag( ray, initPosition, axes, initAxis, initHitPos ); 84 | if( pos ){ 85 | state.position = pos; 86 | return true; 87 | } 88 | break; } 89 | 90 | case Modes.Rotate:{ 91 | const rot = Rotation.rayDrag( ray, initPosition, axes, initAxis, initHitPos ); 92 | if( rot ){ 93 | state.rotation = quat_mul( [0,0,0,1], rot, initRotation ); 94 | return true; 95 | } 96 | break; } 97 | 98 | case Modes.Scale:{ 99 | const scl = Scale.rayDrag( ray, initPosition, axes, initAxis, initHitPos ); 100 | if( scl ){ 101 | 102 | state.position = vec3_scaleAndAdd( [0,0,0], initPosition, axes[ initAxis ], scl[ initAxis ] * 0.5 ); 103 | 104 | 105 | //vec3_inv_scale( scl, scl, state.scaleIncStep ); 106 | vec3_add( scl, initScale, scl ); 107 | state.scale = scl; 108 | 109 | 110 | return true; 111 | } 112 | break; } 113 | } 114 | 115 | return false; 116 | }; 117 | // #endregion 118 | 119 | return { start, stop, move }; 120 | } -------------------------------------------------------------------------------- /test/new2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 113 | -------------------------------------------------------------------------------- /src2/ManipulatorActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | // vec3_add, 3 | // vec3_sub, 4 | // vec3_len, 5 | vec3_copy, 6 | // vec3_transformQuat, 7 | // vec3_norm, 8 | // vec3_dot, 9 | // vec3_scale, 10 | // vec3_scaleAndAdd, 11 | vec3_sqrLen, 12 | quat_mul, 13 | quat_normalize, 14 | // quat_copy, 15 | // quat_sqrLen, 16 | quat_setAxisAngle, 17 | } from '../src/Maths.js'; 18 | 19 | import ManipulatorMode from './ManipulatorMode.js'; 20 | 21 | export default class ManipulatorActions{ 22 | constructor( man ){ 23 | this.man = man; 24 | } 25 | 26 | rayIntersect( ray ){ 27 | const s = this.man.state; 28 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | // Test Axis 30 | const posHit = [Infinity,Infinity,Infinity]; 31 | const posAxis = s.position.rayTest( ray, s, posHit ); 32 | const posDist = vec3_sqrLen( ray.posStart, posHit ); 33 | 34 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | // Test Rotation Arcs 36 | const rotHit = [Infinity,Infinity,Infinity]; 37 | const rotAxis = s.rotation.rayTest( ray, s, rotHit ); 38 | const rotDist = vec3_sqrLen( ray.posStart, rotHit ); 39 | 40 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | if( posAxis === -1 && rotAxis === -1 ){ 42 | s.mode = ManipulatorMode.None; 43 | s.iAxis = -1; 44 | return false; 45 | } 46 | 47 | if( posDist < rotDist ){ 48 | //console.log( 'POSITION', posAxis ); 49 | s.mode = ManipulatorMode.Translate; 50 | s.iAxis = posAxis; 51 | vec3_copy( s.contactPos, posHit ); 52 | }else{ 53 | //console.log( 'ROTATION', rotAxis ); 54 | s.mode = ManipulatorMode.Rotate; 55 | s.iAxis = rotAxis; 56 | vec3_copy( s.contactPos, rotHit ); 57 | } 58 | 59 | return true; 60 | } 61 | 62 | rayHover( ray ){ 63 | return ( this.man.state.isActive && !this.man.state.isDragging && this.rayIntersect( ray ) ); 64 | } 65 | 66 | rayDown( ray ){ 67 | if( this.man.state.isActive && this.rayIntersect( ray ) ){ 68 | this.man.startDrag(); 69 | return true; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | rayMove( ray ){ 76 | if( this.man.state.isActive && !this.man.state.isDragging ) return false; 77 | 78 | const s = this.man.state; 79 | const dist = this.man.trace.rayIntersect( ray ); 80 | 81 | switch( s.mode ){ 82 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83 | case ManipulatorMode.Translate:{ 84 | //const pos = vec3_sub( [0,0,0], this.man.trace.hitPos, this.man.trace.offset ); 85 | s.position.set( this.man.trace.pos ); 86 | s.updateData(); 87 | break; } 88 | 89 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 90 | case ManipulatorMode.Rotate:{ 91 | const distStep = 0.1; // Distance to travel to count as 1 step 92 | const steps = dist / distStep; 93 | const rad = s.rotateStep * steps; 94 | const q = quat_setAxisAngle( [0,0,0,1], s._initAxis, rad ); 95 | 96 | quat_mul( q, q, s._initRotation ); 97 | quat_normalize( q, q ); 98 | 99 | s.rotation.set( q ); 100 | s.rotation.updateAxis(); 101 | s.rotation.updateScale( s ); // TODO This is dumb 102 | s.updateData(); 103 | break; } 104 | 105 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 106 | case ManipulatorMode.Scale: { 107 | 108 | break; } 109 | } 110 | 111 | return true; 112 | } 113 | 114 | rayUp(){ 115 | if( this.man.state.isDragging ){ 116 | this.man.endDrag(); 117 | return true; 118 | } 119 | return false; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /annotatebox/mod/Faces.js: -------------------------------------------------------------------------------- 1 | import { vec3_dot } from '../../src/Maths.js'; 2 | import { vec3_add_batch, vec3_len, vec3_sub, vec3_scale, vec3_scaleAndAdd } from '../lib/Maths.js'; 3 | import { 4 | // Ray, 5 | intersectQuad, 6 | //intersectSphere, 7 | //nearPoint, 8 | NearSegmentResult, 9 | nearSegment, 10 | } from '../lib/RayIntersection.js'; 11 | 12 | 13 | export default class Faces{ 14 | static minLen = 0.15; 15 | 16 | static pnts = [ 17 | [0,0,0], [0,0,0], [0,0,0], [0,0,0], // Bottom : Start back left, front left, front right, back right 18 | [0,0,0], [0,0,0], [0,0,0], [0,0,0], // Top 19 | ]; 20 | 21 | static faces = [ 22 | [ this.pnts[6], this.pnts[2], this.pnts[3], this.pnts[7] ], // Right Face +x 23 | [ this.pnts[4], this.pnts[5], this.pnts[6], this.pnts[7] ], // Top Face +y 24 | [ this.pnts[1], this.pnts[2], this.pnts[6], this.pnts[5] ], // Front face +z 25 | [ this.pnts[4], this.pnts[0], this.pnts[1], this.pnts[5] ], // Left Face -x 26 | [ this.pnts[1], this.pnts[0], this.pnts[3], this.pnts[2] ], // Bot Face -y 27 | [ this.pnts[7], this.pnts[3], this.pnts[0], this.pnts[4] ], // Back Face -z 28 | ]; 29 | 30 | static rayTest( ray, state, axes, hitPos=[0,0,0] ){ 31 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 32 | const pnts = this.pnts; 33 | const ax = new Array( 6 ); 34 | for( let i=0; i < 6; i++ ){ 35 | ax[ i ] = vec3_scale( [0,0,0], axes[i], state.axesLengths[ i ] ); 36 | } 37 | 38 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | vec3_add_batch( pnts[0], ax[3], ax[4], ax[5], state.position ); // Bottom Face 40 | vec3_add_batch( pnts[1], ax[3], ax[4], ax[2], state.position ); 41 | vec3_add_batch( pnts[2], ax[0], ax[4], ax[2], state.position ); 42 | vec3_add_batch( pnts[3], ax[0], ax[4], ax[5], state.position ); 43 | 44 | vec3_add_batch( pnts[4], ax[3], ax[1], ax[5], state.position ); // Top Face 45 | vec3_add_batch( pnts[5], ax[3], ax[1], ax[2], state.position ); 46 | vec3_add_batch( pnts[6], ax[0], ax[1], ax[2], state.position ); 47 | vec3_add_batch( pnts[7], ax[0], ax[1], ax[5], state.position ); 48 | 49 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | let f; 51 | for( let i=0; i < 6; i++ ){ 52 | f = this.faces[ i ]; 53 | if( intersectQuad( ray, f[0], f[1], f[2], f[3], hitPos, true ) !== null ){ 54 | return i; 55 | } 56 | } 57 | 58 | return -1; 59 | } 60 | 61 | static rayDrag( ray, origin, axes, iAxis, initHit, debug ){ 62 | 63 | 64 | // if( iAxis === 3 ) return null; 65 | 66 | // debug.ln.reset(); 67 | // debug.pnt.reset(); 68 | 69 | const a = vec3_scaleAndAdd( [0,0,0], origin, axes[ iAxis ], 1000 ); 70 | const b = vec3_scaleAndAdd( [0,0,0], origin, axes[ iAxis ], -1000 ); 71 | 72 | // debug.ln.add( a, b, 0x00ff00 ); 73 | 74 | const result = new NearSegmentResult(); 75 | 76 | if( nearSegment( ray, a, b, result ) ){ 77 | 78 | //debug.pnt.add( result.segPosition, 0x00ff00, 10 ); 79 | 80 | 81 | // const dir = vec3_sub( [0,0,0], result.segPosition, initHit ); 82 | //const len = vec3_len( result.segPosition, origin ); 83 | const dir = vec3_sub( [0,0,0], result.segPosition, origin ); 84 | 85 | if( vec3_dot( dir, axes[ iAxis ] ) >= 0 ){ 86 | const len = vec3_len( dir ); 87 | if( len >= this.minLen ){ 88 | //console.log( len ); 89 | return len; 90 | } 91 | } 92 | 93 | 94 | 95 | //const len = vec3_len( dir ); 96 | // const sign = Math.sign( vec3_dot( dir, axes[ iAxis ] ) ); 97 | 98 | // const out = [0,0,0]; 99 | // out[ iAxis ] = len * sign; 100 | 101 | //return out; 102 | } 103 | 104 | return null; 105 | } 106 | } -------------------------------------------------------------------------------- /annotatebox/AnnotateBox.js: -------------------------------------------------------------------------------- 1 | // #region IMPORT 2 | import StateProxy from './lib/StateProxy.js'; 3 | import EventDispatcher from './lib/EventDispatcher.js'; 4 | import MouseEvents from './lib/MouseEvents.js'; 5 | import Actions from './Actions.js'; 6 | import { Modes, Axes } from './Structs.js'; 7 | import { debounce } from './lib/Func.js'; 8 | 9 | import Mesh3js from './render/Mesh3js.js'; 10 | 11 | import { 12 | // vec3_add, 13 | vec3_add_batch, 14 | // vec3_sub, 15 | // vec3_len, 16 | // vec3_norm, 17 | // vec3_scale, 18 | vec3_transformQuat, 19 | vec3_negate, 20 | // vec3_sqrLen, 21 | // vec3_copy, 22 | // quat_mul, 23 | // quat_copy, 24 | // quat_setAxisAngle, 25 | // quat_normalize, 26 | } from './lib/Maths.js'; 27 | 28 | // #endregion 29 | 30 | export default function AnnotateBox( oCamera, oRenderer, debug ){ 31 | // #region MAIN 32 | const self = {}; 33 | const state = StateProxy.new({ 34 | position : [0,0,0], 35 | rotation : [0,0,0,1], 36 | axesLengths : [1,1,1,1,1,1], // xp, yp, zp, xn, yn, zn 37 | 38 | isActive : true, 39 | isDragging : false, 40 | 41 | selectMode : Modes.None, 42 | selectAxis : Axes.None, 43 | rayHitPos : null, 44 | 45 | axisLen : 1, // How long the axis line should be on the UI 46 | debug : debug, 47 | }); 48 | 49 | const events = EventDispatcher(); 50 | const actions = Actions( self, state, events ); 51 | const mouse = MouseEvents( oCamera, oRenderer, actions, state, true ); 52 | const mesh = new Mesh3js(); 53 | // #endregion 54 | 55 | // #region METHODS 56 | const getAxes = ()=>{ 57 | const x = vec3_transformQuat( [0,0,0], [1,0,0], state.rotation ); 58 | const y = vec3_transformQuat( [0,0,0], [0,1,0], state.rotation ); 59 | const z = vec3_transformQuat( [0,0,0], [0,0,1], state.rotation ); 60 | return [ 61 | x, y, z, 62 | vec3_negate( [0,0,0], x ), 63 | vec3_negate( [0,0,0], y ), 64 | vec3_negate( [0,0,0], z ), 65 | ]; 66 | }; 67 | 68 | const render = ()=>mesh.render( self ); 69 | const renderDelay = debounce( render, 5 ); 70 | // const getPoints = ()=>{ 71 | // const axes = getAxes(); 72 | // return [ 73 | // vec3_add_batch( [0,0,0], axes[3], axes[4], axes[5], state.position ), 74 | // vec3_add_batch( [0,0,0], axes[3], axes[4], axes[2], state.position ), 75 | // vec3_add_batch( [0,0,0], axes[0], axes[4], axes[2], state.position ), 76 | // vec3_add_batch( [0,0,0], axes[0], axes[4], axes[5], state.position ), 77 | 78 | // vec3_add_batch( [0,0,0], axes[3], axes[1], axes[5], state.position ), 79 | // vec3_add_batch( [0,0,0], axes[3], axes[1], axes[2], state.position ), 80 | // vec3_add_batch( [0,0,0], axes[0], axes[1], axes[2], state.position ), 81 | // vec3_add_batch( [0,0,0], axes[0], axes[1], axes[5], state.position ), 82 | // ]; 83 | // }; 84 | // #endregion 85 | 86 | // #region EVENTS 87 | state.$.on( 'change', e=>{ 88 | // console.log( 'chg', e.detail ); 89 | switch( e.detail.prop ){ 90 | case 'position' : 91 | case 'rotation' : 92 | case 'selectMode' : 93 | case 'selectAxis' : renderDelay(); break; 94 | case 'axesLengths' : render(); break; 95 | } 96 | }); 97 | // #endregion 98 | 99 | // #region Export 100 | self.state = state; 101 | self.getAxes = getAxes; 102 | self.mesh = mesh; 103 | self.events = events; 104 | 105 | self.getPosition = ()=>state.position.slice(), 106 | self.setPosition = v=>{ state.position = [ v[0], v[1], v[2] ]; return self; }, 107 | self.getRotation = ()=>state.rotation.slice(), 108 | self.setRotation = v=>{ state.rotation = [ v[0], v[1], v[2], v[3] ]; return self; }, 109 | 110 | render(); 111 | return self; 112 | // #endregion 113 | } -------------------------------------------------------------------------------- /src2/state/RotationState.js: -------------------------------------------------------------------------------- 1 | import { 2 | vec3_add, 3 | vec3_sub, 4 | vec3_len, 5 | vec3_copy, 6 | vec3_transformQuat, 7 | // vec3_norm, 8 | vec3_dot, 9 | vec3_scale, 10 | // vec3_scaleAndAdd, 11 | vec3_sqrLen, 12 | // quat_mul, 13 | // quat_normalize, 14 | // quat_copy, 15 | // quat_sqrLen, 16 | quat_setAxisAngle, 17 | } from '../../src/Maths.js'; 18 | 19 | import { 20 | // Ray, 21 | intersectPlane, 22 | // intersectTri, 23 | // intersectSphere, 24 | // nearPoint, 25 | // NearSegmentResult, 26 | // nearSegment, 27 | } from '../../src/RayIntersection.js'; 28 | 29 | export default class RotationState{ 30 | minRayDistance = 0.2; 31 | 32 | value = [0,0,0,1]; 33 | xAxis = [1,0,0]; // Actual Axis of rotation 34 | yAxis = [0,1,0]; 35 | zAxis = [0,0,1]; 36 | 37 | xAxisScl = [1,0,0]; // Scaled to Camera Distance & Radius 38 | yAxisScl = [0,1,0]; 39 | zAxisScl = [0,0,1]; 40 | 41 | xAxisSclDir = [1,0,0]; // Normalized, no radius applied 42 | yAxisSclDir = [0,1,0]; 43 | zAxisSclDir = [0,0,1]; 44 | 45 | aryAxisSclDir = [ this.xAxisSclDir, this.yAxisSclDir, this.zAxisSclDir ]; 46 | aryAxis = [ this.xAxis, this.yAxis, this.zAxis ]; 47 | 48 | set( v ){ 49 | this.value[ 0 ] = v[ 0 ]; 50 | this.value[ 1 ] = v[ 1 ]; 51 | this.value[ 2 ] = v[ 2 ]; 52 | this.value[ 3 ] = v[ 3 ]; 53 | } 54 | 55 | setAxisAngle( axis, rad ){ 56 | quat_setAxisAngle( this.value, axis, rad ); 57 | this.updateAxis(); 58 | } 59 | 60 | updateAxis(){ 61 | vec3_transformQuat( this.xAxis, [1,0,0], this.value ); 62 | vec3_transformQuat( this.yAxis, [0,1,0], this.value ); 63 | vec3_transformQuat( this.zAxis, [0,0,1], this.value ); 64 | } 65 | 66 | // Update acaled axis directions based on the camera 67 | updateScale( man ){ 68 | vec3_scale( this.xAxisSclDir, this.xAxis, man.cameraVecScale[ 0 ] ); 69 | vec3_scale( this.yAxisSclDir, this.yAxis, man.cameraVecScale[ 1 ] ); 70 | vec3_scale( this.zAxisSclDir, this.zAxis, man.cameraVecScale[ 2 ] ); 71 | 72 | vec3_scale( this.xAxisScl, this.xAxisSclDir, man.radius ); 73 | vec3_scale( this.yAxisScl, this.yAxisSclDir, man.radius ); 74 | vec3_scale( this.zAxisScl, this.zAxisSclDir, man.radius ); 75 | } 76 | 77 | rayTest( ray, state, out=[0,0,0] ){ 78 | const minRange = state.cameraScale * this.minRayDistance; 79 | const radius = state.cameraScale * state.radius; 80 | const pos = state.position.value; 81 | const hitDir = [0,0,0]; 82 | const hitPos = [0,0,0]; 83 | 84 | let t; 85 | let dist; 86 | 87 | for( let i=0; i < 3; i++ ){ 88 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 89 | // First test against the plane using the axis as the plane normal 90 | t = intersectPlane( ray, pos, this.aryAxisSclDir[i] ); 91 | if( t === null ) continue; 92 | 93 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 94 | // Next do a circle radius test of the hit point to plane origin 95 | ray.posAt( t, hitPos ); 96 | dist = vec3_len( pos, hitPos ); 97 | 98 | if( Math.abs( dist - radius ) <= minRange ){ 99 | // Inside circle, Check if in the positive side of 100 | // the hemisphere using the next axis direction 101 | vec3_sub( hitDir, hitPos, pos ); 102 | 103 | if( vec3_dot( hitDir, this.aryAxisSclDir[ ( i + 1 ) % 3 ] ) >= 0 ){ 104 | 105 | // Do the other hemisphere check with the remaining axis 106 | if( vec3_dot( hitDir, this.aryAxisSclDir[ ( i + 2 ) % 3 ] ) >= 0 ){ 107 | // Debug.pnt2.add( hitPos, 0x00ff00, 5 ); 108 | // Debug.pnt2._updateGeometry(); 109 | vec3_copy( out, hitPos ); 110 | return i; 111 | } 112 | 113 | } 114 | } 115 | } 116 | 117 | return -1; 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /src3/Manipulator3D copy.js: -------------------------------------------------------------------------------- 1 | // import ManipulatorState from './state/ManipulatorState.js'; 2 | // import ManipulatorActions from './ManipulatorActions.js'; 3 | // import MouseEvents from './MouseEvents.js'; 4 | // import Tracer from './Tracer.js'; 5 | 6 | import StateProxy from './lib/StateProxy.js'; 7 | import MouseEvents from './lib/MouseEvents.js'; 8 | 9 | // https://github.com/pmndrs/drei/blob/master/src/core/pivotControls/AxisRotator.tsx 10 | 11 | export default class Manipulator3D{ 12 | // #region MAIN 13 | constructor( camera, renderer, loadMesh=true ){ 14 | this.target = null; 15 | this.camera = camera; 16 | this.mouseEvents = new MouseEvents( this, renderer, false ); 17 | 18 | this.state = StateProxy.new({ 19 | position : [0,0,0], 20 | rotation : [0,0,0,1], 21 | scale : [1,1,1], 22 | isActive : false, 23 | isDragging : false, 24 | }); 25 | 26 | this.settings = { 27 | }; 28 | } 29 | // #endregion 30 | 31 | // #region METHODS 32 | attach( obj ){ this.target = obj; return this; } 33 | detach(){ this.target = null; return this; } 34 | // #endregion 35 | 36 | // #region GETTERS / SETTERS 37 | // #endregion 38 | 39 | // #region CAMERA 40 | updateFromCamera( pos, rot ){ 41 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | // Only update if the camera has changed since last update 43 | if( 44 | Math.abs( vec3_sqrLen( pos, this.cameraPos ) ) <= 0.000001 && 45 | Math.abs( quat_sqrLen( rot, this.cameraRot ) ) <= 0.000001 46 | ) return; 47 | 48 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | vec3_copy( this.cameraPos, pos ); // Save camera state 50 | quat_copy( this.cameraRot, rot ); 51 | this.updateCameraScale(); // Update cameral scaling 52 | this.updateData(); // Update other bits of data 53 | return this; 54 | } 55 | 56 | updateCameraScale(){ 57 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | // Adjust the scale to keep the gizmo as the same size no matter how far the camera goes 59 | const eyeDir = vec3_sub( [0,0,0], this.cameraPos, this.position.value ); 60 | const eyeLen = vec3_len( eyeDir ); 61 | this.cameraScale = eyeLen / this.cameraFactor; 62 | 63 | vec3_norm( eyeDir, eyeDir ); // Normalize for DOT Checks 64 | vec3_scale( this.cameraVecScale, [1,1,1], this.cameraScale ); 65 | 66 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 67 | // Flip viewing to the opposite side 68 | if( vec3_dot( eyeDir, [1,0,0] ) < this.cameraFlipDMin ) this.cameraVecScale[0] = -this.cameraVecScale[0]; 69 | if( vec3_dot( eyeDir, [0,1,0] ) < this.cameraFlipDMin ) this.cameraVecScale[1] = -this.cameraVecScale[1]; 70 | if( vec3_dot( eyeDir, [0,0,1] ) < this.cameraFlipDMin ) this.cameraVecScale[2] = -this.cameraVecScale[2]; 71 | } 72 | // #endregion 73 | 74 | // #region DRAGGING 75 | startDrag(){ 76 | this.state.isDragging = true; 77 | // this.trace.prepare( this ); 78 | // this.state.doCache(); 79 | 80 | // this.emit( 'dragstart' ); 81 | } 82 | 83 | endDrag(){ 84 | this.state.isDragging = false; 85 | //console.log( 'endDrag' ); 86 | 87 | // // onTranslate updates position, need to recalculate at the end of dragging 88 | // // for intersection tests to be accurate. 89 | // this.data.calcAxesPosition(); 90 | 91 | // // When doing dragging away from ui, the hover event won't trigger to undo 92 | // // visual states, so call method at the end of the dragging to tidy things up. 93 | // this.data.resetState(); 94 | // this.mesh.showGizmo(); 95 | 96 | this.emit( 'dragend' ); 97 | } 98 | // #endregion 99 | 100 | // #region OUTER EVENTS 101 | // TODO : Maybe use EventTarget instead of body 102 | on( evtName, fn ){ document.body.addEventListener( evtName, fn ); return this; } 103 | off( evtName, fn ){ document.body.removeEventListener( evtName, fn ); return this; } 104 | emit( evtName, detail=null ){ 105 | document.body.dispatchEvent( new CustomEvent( evtName, { detail, bubbles:true, cancelable:true, composed:false } ) ); 106 | } 107 | // #endregion 108 | } -------------------------------------------------------------------------------- /src3/Actions.js: -------------------------------------------------------------------------------- 1 | // #region IMPORTS 2 | import Translation from './mod/Translation.js'; 3 | import Rotation from './mod/Rotation.js'; 4 | import Scale from './mod/Scale.js'; 5 | import Plane from './mod/Plane.js'; 6 | 7 | import DragAction from './DragAction.js'; 8 | import { Modes, Axes } from './Structs.js'; 9 | 10 | import { 11 | // vec3_add, 12 | // vec3_sub, 13 | // vec3_len, 14 | // vec3_norm, 15 | // vec3_scale, 16 | vec3_transformQuat, 17 | vec3_sqrLen, 18 | // vec3_copy, 19 | // quat_mul, 20 | // quat_copy, 21 | // quat_setAxisAngle, 22 | // quat_normalize, 23 | } from './lib/Maths.js'; 24 | // #endregion 25 | 26 | export default function Actions( state, events ){ 27 | // #region MAIN 28 | const dragging = DragAction( state, events ); 29 | const axes = [ 30 | [1,0,0], 31 | [0,1,0], 32 | [0,0,1], 33 | ]; 34 | // #endregion 35 | 36 | // #region HELPERS 37 | const rayIntersect = ray => { 38 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | // Get the current state axis from rotation 40 | vec3_transformQuat( axes[0], [1,0,0], state.rotation ); 41 | vec3_transformQuat( axes[1], [0,1,0], state.rotation ); 42 | vec3_transformQuat( axes[2], [0,0,1], state.rotation ); 43 | 44 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | // Test Translation Segments 46 | const posHit = [Infinity,Infinity,Infinity]; 47 | const posAxis = Translation.rayTest( ray, state, axes, posHit ); 48 | const posDist = ( posAxis === -1 )? Infinity : vec3_sqrLen( ray.posStart, posHit ); 49 | 50 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | // Test Rotation Arcs 52 | const rotHit = [Infinity,Infinity,Infinity]; 53 | const rotAxis = Rotation.rayTest( ray, state, axes, rotHit ); 54 | const rotDist = ( rotAxis === -1 )? Infinity : vec3_sqrLen( ray.posStart, rotHit ); 55 | 56 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | // Test Scale Points 58 | const sclHit = [Infinity,Infinity,Infinity]; 59 | const sclAxis = Scale.rayTest( ray, state, axes, sclHit ); 60 | 61 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | const plnHit = [Infinity,Infinity,Infinity]; 63 | const plnAxis = Plane.rayTest( ray, state, axes, plnHit ); 64 | const plnDist = ( plnAxis === -1 )? Infinity : vec3_sqrLen( ray.posStart, plnHit ); 65 | 66 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 67 | //console.log( 'Position', posAxis, posDist, 'Rotation', rotAxis, rotDist ); 68 | 69 | if( posAxis === Axes.None && rotAxis === Axes.None && sclAxis === Axes.None && plnAxis === Axes.None ){ 70 | state.selectAxis = Axes.None; 71 | state.selectMode = Modes.None; 72 | return false; 73 | } 74 | 75 | if( sclAxis !== -1 ){ 76 | state.selectAxis = sclAxis; 77 | state.selectMode = Modes.Scale; 78 | state.rayHitPos = sclHit; 79 | 80 | }else if( posAxis !== -1 && posDist < rotDist && posDist < plnDist ){ 81 | state.selectAxis = posAxis; 82 | state.selectMode = Modes.Translate; 83 | state.rayHitPos = posHit; 84 | 85 | }else if( rotAxis !== -1 && rotDist < plnDist ){ 86 | state.selectAxis = rotAxis; 87 | state.selectMode = Modes.Rotate; 88 | state.rayHitPos = rotHit; 89 | 90 | }else if( plnAxis !== -1 ){ 91 | state.selectAxis = plnAxis; 92 | state.selectMode = Modes.Plane; 93 | state.rayHitPos = plnHit; 94 | } 95 | 96 | return true; 97 | }; 98 | // #endregion 99 | 100 | // #region EXPORT 101 | return { 102 | rayDown( ray ){ 103 | if( state.isActive && rayIntersect( ray ) ){ 104 | dragging.start(); 105 | return true; 106 | } 107 | return false; 108 | }, 109 | 110 | rayUp( ray ){ 111 | if( state.isDragging ) dragging.stop (); 112 | }, 113 | 114 | rayMove( ray ){ 115 | if( !state.isActive || !state.isDragging ) return false; 116 | return dragging.move( ray ); 117 | }, 118 | 119 | rayHover( ray ){ 120 | return ( state.isActive && !state.isDragging )? rayIntersect( ray ) : false; 121 | }, 122 | }; 123 | // #endregion 124 | } 125 | -------------------------------------------------------------------------------- /src3/Manipulator3D.js: -------------------------------------------------------------------------------- 1 | // #region IMPORTS 2 | import { Vector3, Quaternion } from 'three'; 3 | 4 | import StateProxy from './lib/StateProxy.js'; 5 | import MouseEvents from './lib/MouseEvents.js'; 6 | import EventDispatcher from './lib/EventDispatcher.js'; 7 | import Mesh3JS from './render/Mesh3JS.js'; 8 | import { Modes, Axes } from './Structs.js'; 9 | import Actions from './Actions.js'; 10 | import { debounce } from './lib/Func.js'; 11 | 12 | import { 13 | // vec3_add, 14 | vec3_sub, 15 | vec3_len, 16 | // vec3_norm, 17 | // vec3_scale, 18 | // vec3_transformQuat, 19 | // vec3_sqrLen, 20 | // vec3_copy, 21 | // quat_mul, 22 | // quat_copy, 23 | // quat_setAxisAngle, 24 | // quat_normalize, 25 | } from './lib/Maths.js'; 26 | // #endregion 27 | 28 | let Debug; 29 | export default function Manipulator3D( oCamera, oRenderer, debug ){ 30 | // #region MAIN 31 | Debug = debug; 32 | const state = StateProxy.new({ 33 | position : [0,0,0], 34 | rotation : [0,0,0,1], 35 | scale : [1,1,1], 36 | isActive : true, 37 | isDragging : false, 38 | target : null, 39 | 40 | rayHitPos : [0,0,0], 41 | 42 | selectMode : Modes.None, 43 | selectAxis : Axes.None, 44 | 45 | usePosition : true, 46 | useRotation : true, 47 | useScale : true, 48 | 49 | cameraFactor : 9, 50 | cameraScale : 1, 51 | 52 | axisLength : 0.85, 53 | arcRadius : 1.08, 54 | pntRadius : 1, 55 | scaleIncStep : 1.0, 56 | }); 57 | 58 | const events = EventDispatcher(); 59 | const actions = Actions( state, events ); 60 | const mouse = MouseEvents( oCamera, oRenderer, actions, state, true ); 61 | const mesh = new Mesh3JS( state ); 62 | // #endregion 63 | 64 | // #region FUNCS 65 | const cameraScaleUpdate = ()=>{ 66 | const scl = computeCameraScale( oCamera, state ); 67 | mesh.scale.set( scl, scl, scl ); 68 | } 69 | 70 | const render = debounce( ()=>{ mesh.render( state ); }, 5 ); 71 | const update = ()=>{ cameraScaleUpdate(); }; 72 | // #endregion 73 | 74 | // #region EVENTS 75 | state.$.on( 'change', e=>{ 76 | // console.log( 'chg', e.detail ); 77 | switch( e.detail.prop ){ 78 | case 'rotation' : 79 | case 'position' : 80 | case 'selectMode' : render(); break; 81 | } 82 | }) 83 | 84 | .on( 'positionChange', ()=>{ 85 | if( state.target ) state.target.position.fromArray( state.position ); 86 | events.emit( 'transform', { position: state.position.slice( 0 ) } ); 87 | }) 88 | 89 | .on( 'rotationChange', ()=>{ 90 | if( state.target ) state.target.quaternion.fromArray( state.rotation ); 91 | events.emit( 'transform', { rotation: state.rotation.slice( 0 ) } ); 92 | }) 93 | 94 | .on( 'scaleChange', ()=>{ 95 | if( state.target ) state.target.scale.fromArray( state.scale ); 96 | events.emit( 'transform', { scale: state.scale.slice( 0 ) } ); 97 | }) 98 | ; 99 | // #endregion 100 | 101 | // #region EXPORT 102 | const self = { 103 | events, 104 | state, 105 | update, 106 | mesh, 107 | 108 | setRotation : v=>{ state.rotation = [ v[0], v[1], v[2], v[3] ]; return self; }, 109 | setPosition : v=>{ state.position = [ v[0], v[1], v[2] ]; return self; }, 110 | attach : o=>{ 111 | state.target = o; 112 | state.position = o.position.toArray(); 113 | state.rotation = o.quaternion.toArray(); 114 | state.scale = o.scale.toArray(); 115 | return self; 116 | }, 117 | }; 118 | return self; 119 | // #endregion 120 | }; 121 | 122 | // #region HELPERS 123 | function computeCameraScale( camera, state ){ 124 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | // Need world space info incase camera is attached to something 126 | const wPos = new Vector3(); 127 | //const wRot = new Quaternion(); 128 | camera.getWorldPosition( wPos ); 129 | //camera.getWorldQuaternion( wRot ); 130 | 131 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 132 | const eyeDir = vec3_sub( [0,0,0], wPos.toArray(), state.position ); 133 | const eyeLen = vec3_len( eyeDir ); 134 | const cameraScale = eyeLen / state.cameraFactor; 135 | 136 | //state.$.update( { cameraScale } ); 137 | state.cameraScale = cameraScale; 138 | return cameraScale; 139 | } 140 | // #endregion 141 | -------------------------------------------------------------------------------- /test/new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 147 | -------------------------------------------------------------------------------- /annotatebox/render/Mesh3js.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import FloorCube from '../../src3/render/FloorCube.js'; 3 | import ScaleCube from './ScaleCube.js'; 4 | import ShapePointsMesh from '../../test/_lib/ShapePointsMesh.js'; 5 | 6 | import { Modes, Axes } from '../Structs.js'; 7 | 8 | import { 9 | // vec3_add, 10 | // vec3_add_batch, 11 | // vec3_sub, 12 | // vec3_len, 13 | // vec3_norm, 14 | vec3_scale, 15 | vec3_scaleAndAdd, 16 | // vec3_transformQuat, 17 | // vec3_negate, 18 | // vec3_sqrLen, 19 | // vec3_copy, 20 | // quat_mul, 21 | // quat_copy, 22 | // quat_setAxisAngle, 23 | // quat_normalize, 24 | } from '../lib/Maths.js'; 25 | 26 | function newMesh( geo, color, pos=null, rot=null, order=0 ){ 27 | const mat = new THREE.MeshBasicMaterial( { 28 | depthTest : false, 29 | depthWrite : false, 30 | fog : false, 31 | toneMapped : false, 32 | transparent : true, 33 | side : THREE.DoubleSide, 34 | opacity : 0.9, 35 | color : color, 36 | } ); 37 | 38 | const mesh = new THREE.Mesh( geo, mat ); 39 | mesh.renderOrder = order; 40 | if( pos ) mesh.position.fromArray( pos ); 41 | if( rot ) mesh.rotation.fromArray( rot ); 42 | return mesh; 43 | } 44 | 45 | 46 | export default class Mesh3js extends THREE.Group{ 47 | axisColors = [ 0x81D773, 0x6DA9EA, 0xF7716A ]; 48 | axes = [ [1,0,0], [0,1,0], [0,0,1] ]; 49 | axisLines = []; 50 | axisArcs = []; 51 | 52 | dmesh = new ScaleCube(); 53 | pnt = new ShapePointsMesh();//.useDepth( true ); 54 | 55 | constructor(){ 56 | super(); 57 | this.add( this.pnt ); 58 | this.add( this.dmesh.mesh ); 59 | 60 | 61 | const PIH = Math.PI * 0.5; 62 | const PIY = Math.PI * 0.375; 63 | const PIQ = Math.PI * 0.25; 64 | const PIX = Math.PI * 0.125; 65 | const PI = Math.PI; 66 | 67 | const geoBox = FloorCube.geo( 0.1, 1.0, 0.1 ); 68 | const xpBox = newMesh( geoBox, this.axisColors[0], null, [0,0,-PIH] ); 69 | const ypBox = newMesh( geoBox, this.axisColors[1], null ); 70 | const zpBox = newMesh( geoBox, this.axisColors[2], null, [PIH,0,0] ); 71 | this.add( xpBox, ypBox, zpBox ); 72 | this.axisLines.push( xpBox, ypBox, zpBox ); 73 | 74 | const geoArc = new THREE.TorusGeometry( 1, 0.1, 3, 10, PIQ ); 75 | const zArc = newMesh( geoArc, this.axisColors[2], [1,0,0], [0,0,-PIX] ); 76 | const yArc = newMesh( geoArc, this.axisColors[1], [0,0,1], [PIH,0,PIY] ); 77 | const xArc = newMesh( geoArc, this.axisColors[0], [0,1,0], [0,PIH,PIY] ); 78 | this.add( xArc, yArc, zArc ); 79 | this.axisArcs.push( xArc, yArc, zArc ); 80 | } 81 | 82 | render( abox ){ 83 | this.pnt.reset(); 84 | 85 | const tmp = [0,0,0]; 86 | const pos = abox.getPosition(); 87 | const rot = abox.getRotation(); 88 | const lens = abox.state.axesLengths; 89 | const selAxis = abox.state.selectAxis; 90 | const selMode = abox.state.selectMode; 91 | 92 | //const axes = abox.getAxes(); 93 | //const pnts = abox.getPoints(); 94 | 95 | this.position.fromArray( pos ); 96 | this.quaternion.fromArray( rot ); 97 | 98 | this.dmesh.setAxesLengths( lens ); 99 | this.dmesh.selectFace( (( selMode === Modes.Scale )? selAxis : -1) ); 100 | 101 | for( let i=0; i < 3; i++ ){ 102 | vec3_scale( tmp, this.axes[ i ], lens[ i ] ); 103 | this.axisLines[ i ].position.fromArray( tmp ); 104 | this.axisLines[ i ].material.color.setHex( ( selMode === Modes.Translate && i === selAxis)? 0xffffff : this.axisColors[i] ); 105 | 106 | const ii = ( i+1 ) % 3; 107 | vec3_scale( tmp, this.axes[ii], lens[ii] ); 108 | this.axisArcs[ i ].position.fromArray( tmp ); 109 | this.axisArcs[ i ].material.color.setHex( ( selMode === Modes.Rotate && i === selAxis)? 0xffffff : this.axisColors[i] ); 110 | } 111 | 112 | 113 | 114 | // const offset = 0.4; 115 | // const tmp = [0,0,0]; 116 | this.pnt.add( [0,0,0], 0xffff00, 5 ); 117 | // this.pnt.add( vec3_scale( tmp, [1,0,0], lens[ 0 ] + offset), 0xffffff, 8 ); 118 | // this.pnt.add( vec3_scale( tmp, [-1,0,0], lens[ 3 ] + offset), 0xffffff, 8 ); 119 | // this.pnt.add( vec3_scale( tmp, [0,1,0], lens[ 1 ] + offset), 0xffffff, 8 ); 120 | // this.pnt.add( vec3_scale( tmp, [0,-1,0], lens[ 4 ] + offset), 0xffffff, 8 ); 121 | // this.pnt.add( vec3_scale( tmp, [0,0,1], lens[ 1 ] + offset), 0xffffff, 8 ); 122 | // this.pnt.add( vec3_scale( tmp, [0,0,-1], lens[ 4 ] + offset), 0xffffff, 8 ); 123 | } 124 | } -------------------------------------------------------------------------------- /src2/Tracer.js: -------------------------------------------------------------------------------- 1 | import { 2 | // vec3_add, 3 | vec3_sub, 4 | vec3_len, 5 | vec3_copy, 6 | // vec3_transformQuat, 7 | vec3_norm, 8 | vec3_dot, 9 | vec3_scale, 10 | vec3_scaleAndAdd, 11 | // vec3_sqrLen, 12 | // quat_mul, 13 | // quat_normalize, 14 | // quat_copy, 15 | // quat_sqrLen, 16 | // quat_setAxisAngle, 17 | } from '../src/Maths.js'; 18 | 19 | import { 20 | // Ray, 21 | // intersectPlane, 22 | // intersectTri, 23 | // intersectSphere, 24 | // nearPoint, 25 | NearSegmentResult, 26 | nearSegment, 27 | } from '../src/RayIntersection.js'; 28 | 29 | import ManipulatorMode from './ManipulatorMode.js'; 30 | 31 | export default class Tracer{ 32 | // #region MAIN 33 | initPos = [0,0,0]; // Inital Contact Position that started the drag 34 | offset = [0,0,0]; // Offset vector from contact position and gizmo's origin 35 | startPos = [0,0,0]; // Start Position of the trace line 36 | endPos = [0,0,0]; // End Position of the trace line 37 | dir = [0,0,0]; // Direction of trace line 38 | hitPos = [0,0,0]; // Intersection position of mouse ray & trace segment 39 | pos = [0,0,0]; // Final position with offset applied 40 | distance = 0; // Signed distance traveled 41 | // #endregion 42 | 43 | rayIntersect( ray ){ 44 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | // Trace mouse movement onto trace line 46 | const segResult = new NearSegmentResult(); 47 | nearSegment( ray, this.startPos, this.endPos, segResult ); 48 | vec3_copy( this.hitPos, segResult.segPosition ); 49 | 50 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | // Distance Traveled 52 | const dir = vec3_sub( [0,0,0], this.hitPos, this.initPos ); 53 | const sign = Math.sign( vec3_dot( this.dir, dir ) ); 54 | this.distance = vec3_len( dir ) * sign; 55 | 56 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | // Final Position 58 | vec3_sub( this.pos, this.hitPos, this.offset ); 59 | 60 | // Debug.pnt2.reset() 61 | // .add( segResult.segPosition, 0x707070, 8 ) 62 | // .add( this.initPos, 0x00ff00, 4 ); 63 | 64 | return this.distance; 65 | // console.log( dist ); 66 | } 67 | 68 | prepare( man ){ 69 | const s = man.state; 70 | vec3_copy( this.initPos, s.contactPos ); 71 | vec3_sub( this.offset, this.initPos, s.position.value ); 72 | 73 | switch( s.mode ){ 74 | // ---------------------------------------------- 75 | case ManipulatorMode.Translate:{ 76 | vec3_copy( this.dir, s.rotation.aryAxisSclDir[ s.iAxis ] ); 77 | vec3_scaleAndAdd( this.startPos, this.initPos, s.rotation.aryAxisSclDir[ s.iAxis ], 1000 ); 78 | vec3_scaleAndAdd( this.endPos, this.initPos, s.rotation.aryAxisSclDir[ s.iAxis ], -1000 ); 79 | break; } 80 | 81 | // ---------------------------------------------- 82 | case ManipulatorMode.Rotate:{ 83 | const i = (s.iAxis + 1) % 3; 84 | const ii = (s.iAxis + 2) % 3; 85 | const a = s.position.aryEndPoint[ i ]; 86 | const b = s.position.aryEndPoint[ ii ]; 87 | 88 | const sn = 89 | Math.sign( s.cameraVecScale[ i ] ) * 90 | Math.sign( s.cameraVecScale[ ii ] ); 91 | 92 | vec3_sub( this.dir, b, a ); 93 | vec3_norm( this.dir, this.dir ); 94 | vec3_scale( this.dir, this.dir, sn ); 95 | 96 | vec3_scaleAndAdd( this.startPos, this.initPos, this.dir, 1000 ); 97 | vec3_scaleAndAdd( this.endPos, this.initPos, this.dir, -1000 ); 98 | break; } 99 | 100 | // ---------------------------------------------- 101 | case ManipulatorMode.Scale: break; 102 | } 103 | 104 | // Debug.ln2.reset(); 105 | // Debug.ln2.add( this.startPos, this.endPos, 0x707070 ); 106 | 107 | 108 | // console.log( s.mode, s.axis ) 109 | 110 | //main.state.rotation.aryAxisSclDir[]; 111 | 112 | // this.traceLine.isActive = true; 113 | // vec3_copy( this.traceLine.origin, pos ); 114 | // vec3_copy( this.traceLine.hitPos, pos ); 115 | 116 | // if( axis == -1 ){ 117 | // vec3_transformQuat( this.traceLine.dir, [1,0,0], this.lastCamRot ); 118 | // }else{ 119 | // vec3_copy( this.traceLine.dir, this.axes[axis].dir ); 120 | // } 121 | 122 | // vec3_scaleAndAdd( this.traceLine.a, pos, this.traceLine.dir, -1000 ); 123 | // vec3_scaleAndAdd( this.traceLine.b, pos, this.traceLine.dir, 1000 ); 124 | 125 | 126 | //Debug.pnt2.add( man.state.contactPos, 0x00ff00, 5 ); 127 | } 128 | } -------------------------------------------------------------------------------- /test/_lib/Starter.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | 4 | // #region STARTUP 5 | const mod_path = import.meta.url.substring( 0, import.meta.url.lastIndexOf("/") + 1 ); 6 | const css_path = mod_path + "Starter.css"; 7 | 8 | (function(){ 9 | let link = document.createElement( "link" ); 10 | link.rel = "stylesheet"; 11 | link.type = "text/css"; 12 | link.media = "all"; 13 | link.href = css_path; 14 | document.getElementsByTagName( "head" )[0].appendChild( link ); 15 | })(); 16 | // #endregion ///////////////////////////////////////////////////////////////////////// 17 | 18 | 19 | // Boiler Plate Starter for ThreeJS 20 | class Starter{ 21 | // #region MAIN 22 | scene = null; 23 | camera = null; 24 | clock = null; 25 | renderer = null; 26 | orbit = null; 27 | render_bind = this.render.bind( this ); 28 | onRender = null; 29 | deltaTime = 0; 30 | elapsedTime = 0; 31 | 32 | constructor( config={} ){ // { webgl2:true, grid:true, container:null } 33 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 34 | // MAIN 35 | this.scene = new THREE.Scene(); 36 | this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.01, 2000 ); 37 | this.camera.position.set( 0, 10, 20 ); 38 | 39 | this.clock = new THREE.Clock(); 40 | 41 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | // LIGHTING 43 | let light = new THREE.DirectionalLight( 0xffffff, 0.8 ); 44 | light.position.set( 4, 10, 4 ); 45 | 46 | this.scene.add( light ); 47 | this.scene.add( new THREE.AmbientLight( 0x404040 ) ); 48 | 49 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | // RENDERER 51 | let options = { antialias:true, alpha:true }; 52 | 53 | // THREE.JS can't handle loading into WebGL2 on its own 54 | // Need to create canvas & get the proper context, pass those 2 into 3js 55 | if( config.webgl2 ){ 56 | let canvas = document.createElement( "canvas" ); 57 | options.canvas = canvas; 58 | options.context = canvas.getContext( "webgl2" ); 59 | } 60 | 61 | this.renderer = new THREE.WebGLRenderer( options ); 62 | this.renderer.setPixelRatio( window.devicePixelRatio ); 63 | this.renderer.setClearColor( 0x3a3a3a, 1 ); 64 | 65 | //--------------------------------- 66 | // where to add the cnavas object, in a container or in the body. 67 | if( config.container ) config.container.appendChild( this.renderer.domElement ); 68 | else document.body.appendChild( this.renderer.domElement ); 69 | 70 | //--------------------------------- 71 | // Have the canvas set as full screen or fill its container's space 72 | if( config.fullscreen != false ){ 73 | this.setSize( window.innerWidth, window.innerHeight ); 74 | }else{ 75 | // Take the size of the parent element. 76 | const box = this.renderer.domElement.parentNode.getBoundingClientRect(); 77 | this.setSize( box.width , box.height ); 78 | } 79 | 80 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | // MISC 82 | this.orbit = new OrbitControls( this.camera, this.renderer.domElement ); 83 | if( config.grid ) this.scene.add( new THREE.GridHelper( 20, 20, 0x0c610c, 0x444444 ) ); 84 | 85 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 86 | window.addEventListener( 'resize', this.onResize.bind( this ) ); 87 | } 88 | 89 | render(){ 90 | requestAnimationFrame( this.render_bind ); 91 | 92 | this.deltaTime = this.clock.getDelta(); 93 | this.elapsedTime = this.clock.elapsedTime; 94 | 95 | if( this.onRender ) this.onRender( this.deltaTime, this.elapsedTime ); 96 | this.renderer.render( this.scene, this.camera ); 97 | } 98 | // #endregion //////////////////////////////////////////////////////////////////////////////////////// 99 | 100 | // #region METHODS 101 | add( o ){ this.scene.add( o ); return this; } 102 | remove( o ){ this.scene.remove( o ); return this; } 103 | 104 | setCamera( lon, lat, radius, target ){ 105 | let phi = ( 90 - lat ) * Math.PI / 180, 106 | theta = ( lon + 180 ) * Math.PI / 180; 107 | 108 | this.camera.position.set( 109 | -(radius * Math.sin( phi ) * Math.sin(theta)), 110 | radius * Math.cos( phi ), 111 | -(radius * Math.sin( phi ) * Math.cos(theta)) 112 | ); 113 | 114 | if( target ) this.orbit.target.fromArray( target ); 115 | 116 | this.orbit.update(); 117 | return this; 118 | } 119 | 120 | setSize( w, h ){ 121 | this.renderer.setSize( w , h ); 122 | 123 | // When changing the canvas size, need to update the Projection Aspect Ratio to render correctly. 124 | this.camera.aspect = w / h; 125 | this.camera.updateProjectionMatrix(); 126 | } 127 | // #endregion //////////////////////////////////////////////////////////////////////////////////////// 128 | 129 | // #region EVENTS 130 | onResize( e ){ 131 | this.setSize( window.innerWidth, window.innerHeight ); 132 | } 133 | //#endregion 134 | 135 | } 136 | 137 | export default Starter; 138 | export { THREE }; -------------------------------------------------------------------------------- /src3/render/Mesh3JS.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Group, Line, Mesh, DoubleSide, 4 | MeshBasicMaterial, LineBasicMaterial, 5 | BufferGeometry, Float32BufferAttribute, 6 | SphereGeometry, TorusGeometry, CylinderGeometry, BoxGeometry, PlaneGeometry, CircleGeometry, 7 | } from 'three'; 8 | 9 | import FloorCube from './FloorCube.js'; 10 | 11 | function newMesh( geo, color, pos=null, rot=null, order=0 ){ 12 | const mat = new MeshBasicMaterial( { 13 | depthTest : false, 14 | depthWrite : false, 15 | fog : false, 16 | toneMapped : false, 17 | transparent : true, 18 | side : DoubleSide, 19 | opacity : 0.9, 20 | color : color, 21 | } ); 22 | 23 | const mesh = new Mesh( geo, mat ); 24 | mesh.renderOrder = order; 25 | if( pos ) mesh.position.fromArray( pos ); 26 | if( rot ) mesh.rotation.fromArray( rot ); 27 | return mesh; 28 | } 29 | 30 | export default class Mesh3JS extends Group{ 31 | axisColors = [ 0x81D773, 0x6DA9EA, 0xF7716A ]; 32 | axisPLines = []; 33 | axisNLines = []; 34 | axisArc = []; 35 | axisPoints = []; 36 | axisPlanes = []; 37 | //onUpdate = null; 38 | 39 | constructor( state ){ 40 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | super(); 42 | //this.onUpdate = updateCallback; 43 | 44 | const PIH = Math.PI * 0.5; 45 | const PI = Math.PI; 46 | const arcRadius = 0.8; 47 | const arcThickness = 0.025; 48 | const planeSize = 0.25; 49 | const boxSize = 0.04; 50 | const boxLen = state.axisLength; //0.85; 51 | 52 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | // GEOMETRY 54 | const geoSphere = new SphereGeometry( 0.08, 8, 8 ); 55 | const geoArc = new TorusGeometry( arcRadius, arcThickness, 3, 30, PIH ); 56 | //const geoPlane = new PlaneGeometry( planeSize, planeSize ); 57 | //const geoPlane = new CircleGeometry( planeSize, 6 ); 58 | const geoBox = FloorCube.geo( boxSize, boxLen, boxSize ); 59 | 60 | const geoTri = new BufferGeometry(); 61 | geoTri.setAttribute( 'position', new Float32BufferAttribute( [ 0.15,0.15,0, 0.8,0.15,0, 0.15,0.8,0 ], 3 ) ); 62 | 63 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | // Translation 65 | const p = 0.0; 66 | const xpBox = newMesh( geoBox, this.axisColors[0], [p,0,0], [0,0,-PIH] ); 67 | const ypBox = newMesh( geoBox, this.axisColors[1], [0,p,0] ); 68 | const zpBox = newMesh( geoBox, this.axisColors[2], [0,0,p], [PIH,0,0] ); 69 | this.add( xpBox, ypBox, zpBox ); 70 | this.axisPLines.push( xpBox, ypBox, zpBox ); 71 | 72 | const xnBox = newMesh( geoBox, this.axisColors[0], [-p,0,0], [0,0,PIH] ); 73 | const ynBox = newMesh( geoBox, this.axisColors[1], [0,-p,0], [0,0,PI] ); 74 | const znBox = newMesh( geoBox, this.axisColors[2], [0,0,-p], [-PIH,0,0] ); 75 | this.add( xnBox, ynBox, znBox ); 76 | this.axisNLines.push( xnBox, ynBox, znBox ); 77 | 78 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | // Rotation 80 | const r = 0.2; 81 | const zArc = newMesh( geoArc, this.axisColors[2], [r,r,0], [0,0,0] ); 82 | const xArc = newMesh( geoArc, this.axisColors[0], [0,r,r], [0,-PIH,0] ); 83 | const yArc = newMesh( geoArc, this.axisColors[1], [r,0,r], [PIH,0,0] ); 84 | 85 | this.add( xArc, yArc, zArc ); 86 | this.axisArc.push( xArc, yArc, zArc ); 87 | 88 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 89 | // Plane 90 | // const pp = planeSize + 0.15; 91 | // const xPln = newMesh( geoPlane, this.axisColors[0], [0,pp,pp], [0,PIH,0] ); 92 | // const yPln = newMesh( geoPlane, this.axisColors[1], [pp,0,pp], [PIH,0,0] ); 93 | // const zPln = newMesh( geoPlane, this.axisColors[2], [pp,pp,0] ); 94 | 95 | const xPln = newMesh( geoTri, this.axisColors[0], [0,0,0], [0,-PIH,0] ); 96 | const yPln = newMesh( geoTri, this.axisColors[1], [0,0,0], [PIH,0,0] ); 97 | const zPln = newMesh( geoTri, this.axisColors[2], [0,0,0], [0,0,0] ); 98 | this.add( xPln, yPln, zPln ); 99 | this.axisPlanes.push( xPln, yPln, zPln ); 100 | 101 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 102 | // Scale 103 | const s = 1; 104 | const xpScl = newMesh( geoSphere, this.axisColors[0], [s,0,0] ); 105 | const xnScl = newMesh( geoSphere, this.axisColors[0], [-s,0,0] ); 106 | const ypScl = newMesh( geoSphere, this.axisColors[1], [0,s,0] ); 107 | const ynScl = newMesh( geoSphere, this.axisColors[1], [0,-s,0] ); 108 | const zpScl = newMesh( geoSphere, this.axisColors[2], [0,0,s] ); 109 | const znScl = newMesh( geoSphere, this.axisColors[2], [0,0,-s] ); 110 | const mScl = newMesh( geoSphere, 0xffff00, null, null, 1 ); 111 | 112 | this.add( mScl, xpScl, xnScl, ypScl, ynScl, zpScl, znScl ); 113 | this.axisPoints.push( xpScl, ypScl, zpScl, xnScl, ynScl, znScl, mScl ); 114 | } 115 | 116 | render( state ){ 117 | const mode = state.selectMode; 118 | const axis = state.selectAxis; 119 | 120 | let c; 121 | this.quaternion.fromArray( state.rotation ); 122 | this.position.fromArray( state.position ); 123 | 124 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | // LINES 126 | for( let i=0; i < 3; i++ ){ 127 | this.axisNLines[ i ].material.color.setHex( ( mode === 0 && i === axis )? 0xffffff : this.axisColors[i] ); 128 | this.axisPLines[ i ].material.color.setHex( ( mode === 0 && i === axis )? 0xffffff : this.axisColors[i] ); 129 | } 130 | 131 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 132 | // ARCS 133 | for( let i=0; i < 3; i++ ){ 134 | this.axisArc[ i ].material.color.setHex( ( mode === 1 && i === axis)? 0xffffff : this.axisColors[i] ); 135 | } 136 | 137 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 138 | // PLANES 139 | for( let i=0; i < 3; i++ ){ 140 | this.axisPlanes[ i ].material.color.setHex( ( mode === 3 && i === axis)? 0xffffff : this.axisColors[i] ); 141 | } 142 | 143 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 144 | // POINTS 145 | for( let i=0; i < 3; i++ ){ 146 | c = ( mode === 2 && i === axis )? 0xffffff : this.axisColors[ i ]; 147 | this.axisPoints[ i ].material.color.setHex( c ); 148 | this.axisPoints[ i+3 ].material.color.setHex( c ); 149 | } 150 | 151 | c = ( mode === 2 && axis === 3 )? 0xffffff : 0xffff00; 152 | this.axisPoints[ 6 ].material.color.setHex( c ); 153 | } 154 | 155 | /* CAN USE THIS OVERRIDE TO REPLACE UPDATE FUNCTION, IT WILL BE CALLED ON EVERY FRAME*/ 156 | // updateMatrixWorld(){ 157 | // super.updateMatrixWorld( this ); 158 | // if( this.onUpdate ) this.onUpdate(); 159 | // } 160 | } -------------------------------------------------------------------------------- /src3/lib/transform.js: -------------------------------------------------------------------------------- 1 | // https://gabormakesgames.com/blog_transforms_transforms.html 2 | // https://gabormakesgames.com/blog_transforms_transform_world.html 3 | 4 | export default class transform{ 5 | // #region MAIN 6 | static new(){ return { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }; } 7 | 8 | static clone( t ){ return { pos:t.pos.slice(), rot:t.rot.slice(), scl:t.scl.slice() }; } 9 | 10 | static reset( t ){ 11 | t.pos[0] = 0; t.pos[1] = 0; t.pos[2] = 0; 12 | t.scl[0] = 1; t.scl[1] = 1; t.scl[2] = 1; 13 | t.rot[0] = 0; t.rot[1] = 0; t.rot[2] = 0; t.rot[3] = 1; 14 | } 15 | 16 | static copy( t, out ){ 17 | out.pos[ 0 ] = t.pos[ 0 ]; 18 | out.pos[ 1 ] = t.pos[ 1 ]; 19 | out.pos[ 2 ] = t.pos[ 2 ]; 20 | 21 | out.scl[ 0 ] = t.scl[ 0 ]; 22 | out.scl[ 1 ] = t.scl[ 1 ]; 23 | out.scl[ 2 ] = t.scl[ 2 ]; 24 | 25 | out.rot[ 0 ] = t.rot[ 0 ]; 26 | out.rot[ 1 ] = t.rot[ 1 ]; 27 | out.rot[ 2 ] = t.rot[ 2 ]; 28 | out.rot[ 3 ] = t.rot[ 3 ]; 29 | 30 | return out; 31 | } 32 | // #endregion 33 | 34 | // #region OPS 35 | 36 | // fromInvert( t: Transform ) : this{ 37 | // // Invert Rotation 38 | // this.rot.fromInvert( t.rot ); 39 | 40 | // // Invert Scale 41 | // this.scl.fromInvert( t.scl ); 42 | 43 | // // Invert Position : rotInv * ( invScl * -Pos ) 44 | // this.pos 45 | // .fromNegate( t.pos ) 46 | // .mul( this.scl ) 47 | // .transformQuat( this.rot ); 48 | 49 | // return this; 50 | // } 51 | 52 | // Transform LocalInverse(Transform t) { 53 | // Quaternion invRotation = Inverse(t.rotation); 54 | 55 | // Vector3 invScale = Vector3(0, 0, 0); 56 | // if (t.scale.x != 0) { // Do epsilon comparison here 57 | // invScale.x = 1.0 / t.scale.x 58 | // } 59 | // if (t.scale.y != 0) { // Do epsilon comparison here 60 | // invScale.y = 1.0 / t.scale.y 61 | // } 62 | // if (t.scale.z != 0) { // Do epsilon comparison here 63 | // invScale.z = 1.0 / t.scale.z 64 | // } 65 | 66 | // Vector3 invTranslation = invRotation * (invScale * (-1 * t.translation)); 67 | 68 | // Transform result; 69 | // result.position = invTranslation; 70 | // result.rotation = invRotation; 71 | // result.scale = invScale; 72 | 73 | // return result; 74 | // } 75 | 76 | 77 | /* 78 | Matrix ToMatrix(Transform transform) { 79 | // First, extract the rotation basis of the transform 80 | Vector x = Vector(1, 0, 0) * transform.rotation; // Vec3 * Quat (right vector) 81 | Vector y = Vector(0, 1, 0) * transform.rotation; // Vec3 * Quat (up vector) 82 | Vector z = Vector(0, 0, 1) * transform.rotation; // Vec3 * Quat (forward vector) 83 | 84 | // Next, scale the basis vectors 85 | x = x * transform.scale.x; // Vector * float 86 | y = y * transform.scale.y; // Vector * float 87 | z = z * transform.scale.z; // Vector * float 88 | 89 | // Extract the position of the transform 90 | Vector t = transform.position; 91 | 92 | // Create matrix 93 | return Matrix( 94 | x.x, x.y, x.z, 0, // X basis (& Scale) 95 | y.x, y.y, y.z, 0, // Y basis (& scale) 96 | z.x, z.y, z.z, 0, // Z basis (& scale) 97 | t.x, t.y, t.z, 1 // Position 98 | ); 99 | } 100 | */ 101 | 102 | // fromMul( tp: Transform, tc: Transform ) : this{ 103 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 104 | // // POSITION - parent.position + ( parent.rotation * ( parent.scale * child.position ) ) 105 | // const v = new Vec3().fromMul( tp.scl, tc.pos ).transformQuat( tp.rot ); // parent.scale * child.position; 106 | // this.pos.fromAdd( tp.pos, v ); 107 | 108 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 109 | // // SCALE - parent.scale * child.scale 110 | // this.scl.fromMul( tp.scl, tc.scl ); 111 | 112 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 113 | // // ROTATION - parent.rotation * child.rotation 114 | // this.rot.fromMul( tp.rot, tc.rot ); 115 | 116 | // return this; 117 | // } 118 | 119 | // mul( cr: TVec4 | Transform, cp ?: TVec3, cs ?: TVec3 ) : this{ 120 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 121 | // // If just passing in Tranform Object 122 | // if( cr instanceof Transform ){ 123 | // cp = cr.pos; 124 | // cs = cr.scl; 125 | // cr = cr.rot; 126 | // } 127 | 128 | // if( cr && cp ){ 129 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 130 | // // POSITION - parent.position + ( parent.rotation * ( parent.scale * child.position ) ) 131 | // this.pos.add( new Vec3().fromMul( this.scl, cp ).transformQuat( this.rot ) ); 132 | 133 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | // // SCALE - parent.scale * child.scale 135 | // if( cs ) this.scl.mul( cs ); 136 | 137 | // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 138 | // // ROTATION - parent.rotation * child.rotation 139 | // this.rot.mul( cr ); 140 | // } 141 | 142 | // return this; 143 | // } 144 | 145 | // addPos( cp: TVec3, ignoreScl=false ) : this{ 146 | // //POSITION - parent.position + ( parent.rotation * ( parent.scale * child.position ) ) 147 | // if( ignoreScl ) this.pos.add( new Vec3().fromQuat( this.rot, cp ) ); 148 | // else this.pos.add( new Vec3().fromMul( cp, this.scl ).transformQuat( this.rot ) ); 149 | // return this; 150 | // } 151 | 152 | // #endregion 153 | } 154 | 155 | // // #region TRANSFORMATION 156 | // transformVec3( v: TVec3, out ?: TVec3 ) : TVec3{ 157 | // // GLSL - vecQuatRotation(model.rotation, a_position.xyz * model.scale) + model.position; 158 | // // return (out || v) 159 | // // .fromMul( v, this.scl ) 160 | // // .transformQuat( this.rot ) 161 | // // .add( this.pos ); 162 | 163 | // out = out || v; 164 | // vec3.mul( v, this.scl, out ); 165 | // vec3.transformQuat( out, this.rot ); 166 | // vec3.add( out, this.pos ); 167 | // return out; 168 | // } 169 | // // #endregion 170 | 171 | // Vector3 InverseTransformPoint(Transform t, Vector3 point) { 172 | // // Recursive function, apply inverse of parent transform first 173 | // if (t.parent != NULL) { 174 | // point = InverseTransformPoint(t.parent, point) 175 | // } 176 | 177 | // // First, apply the inverse translation of the transform 178 | // point = point - t.position; 179 | 180 | // // Next, apply the inverse rotation of the transform 181 | // Quaternion invRot = Inverse(t.rotation); 182 | // point = point * invRot; 183 | 184 | // // Finally, apply the inverse scale 185 | // point = point / t.scale; // Component wise vector division 186 | 187 | // return point 188 | // } 189 | 190 | /* 191 | World Space Position to Local Space. 192 | V .copy( gBWorld.eye_lid_upper_mid_l.pos ) // World Space Postion 193 | .add( [0, -0.05 * t, 0 ] ) // Change it 194 | .sub( gBWorld.eye_l.pos ) // Subtract from Parent's WS Position 195 | .div( gBWorld.eye_l.scl ) // Div by Parent's WS Scale 196 | .transform_quat( gBWorld.eye_l.rot_inv ); // Rotate by Parent's WS Inverse Rotation 197 | */ -------------------------------------------------------------------------------- /test/annotate_box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 228 | -------------------------------------------------------------------------------- /src/RayIntersection.js: -------------------------------------------------------------------------------- 1 | // #region IMPORT 2 | import { 3 | vec3_add, 4 | vec3_sub, 5 | vec3_mul, 6 | vec3_norm, 7 | vec3_copy, 8 | vec3_scale, 9 | vec3_dot, 10 | vec3_cross, 11 | vec3_sqrLen, 12 | vec3_lerp, 13 | vec3_len, 14 | vec4_transformMat4, 15 | mat4_invert, 16 | mat4_mul, 17 | } from './Maths.js'; 18 | // #endregion 19 | 20 | export class Ray{ 21 | posStart = [0,0,0]; // Origin 22 | posEnd = [0,0,0]; 23 | direction = [0,0,0]; // Direction from Start to End 24 | vecLength = [0,0,0]; // Vector Length between start to end 25 | 26 | // #region GETTERS / SETTERS 27 | /** Get position of the ray from T Scale of VecLen */ 28 | posAt( t, out){ 29 | // RayVecLen * t + RayOrigin 30 | // also works lerp( RayOrigin, RayEnd, t ) 31 | out = out || [0,0,0]; 32 | out[ 0 ] = this.vecLength[ 0 ] * t + this.posStart[ 0 ]; 33 | out[ 1 ] = this.vecLength[ 1 ] * t + this.posStart[ 1 ]; 34 | out[ 2 ] = this.vecLength[ 2 ] * t + this.posStart[ 2 ]; 35 | return out; 36 | } 37 | 38 | /** Get position of the ray from distance from origin */ 39 | directionAt( len, out ){ 40 | out = out || [0,0,0]; 41 | out[ 0 ] = this.direction[ 0 ] * len + this.posStart[ 0 ]; 42 | out[ 1 ] = this.direction[ 1 ] * len + this.posStart[ 1 ]; 43 | out[ 2 ] = this.direction[ 2 ] * len + this.posStart[ 2 ]; 44 | return out; 45 | } 46 | 47 | fromCaster( caster ){ 48 | vec3_copy( this.posStart, caster.ray.origin.toArray() ); 49 | vec3_copy( this.direction, caster.ray.direction.toArray() ); 50 | 51 | const len = ( caster.far == Infinity )? 1000 : caster.far; 52 | vec3_scale( this.vecLength, this.direction, len ); 53 | vec3_add( this.posEnd, this.posStart, this.vecLength ); 54 | } 55 | 56 | fromScreenProjection( x, y, w, h, projMatrix, camMatrix ){ 57 | // http://antongerdelan.net/opengl/raycasting.html 58 | // Normalize Device Coordinate 59 | const nx = x / w * 2 - 1; 60 | const ny = 1 - y / h * 2; 61 | 62 | // inverseWorldMatrix = invert( ProjectionMatrix * ViewMatrix ) OR 63 | // inverseWorldMatrix = localMatrix * invert( ProjectionMatrix ) 64 | const invMatrix = [ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 ]; 65 | mat4_invert( invMatrix, projMatrix ) 66 | mat4_mul( invMatrix, camMatrix, invMatrix ); 67 | 68 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | // https://stackoverflow.com/questions/20140711/picking-in-3d-with-ray-tracing-using-ninevehgl-or-opengl-i-phone/20143963#20143963 70 | // Clip Cords would be [nx,ny,-1,1]; 71 | const clipNear = [ nx, ny, -1, 1 ]; 72 | const clipFar = [ nx, ny, 1, 1 ]; 73 | 74 | // using 4d Homogeneous Clip Coordinates 75 | vec4_transformMat4( clipNear, clipNear, invMatrix ); 76 | vec4_transformMat4( clipFar, clipFar, invMatrix ); 77 | 78 | // Normalize by using W component 79 | for( let i=0; i < 3; i++){ 80 | clipNear[ i ] /= clipNear[ 3 ]; 81 | clipFar [ i ] /= clipFar [ 3 ]; 82 | } 83 | 84 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | vec3_copy( this.posStart, clipNear ); // Starting Point of the Ray 86 | vec3_copy( this.posEnd, clipFar ); // The absolute end of the ray 87 | vec3_sub( this.vecLength, clipFar, clipNear ); // Vector Length 88 | vec3_norm( this.direction, this.vecLength ); // Normalized Vector Length 89 | return this; 90 | } 91 | // #endregion 92 | } 93 | 94 | export function intersectSphere( ray, origin, radius ){ 95 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 96 | const radiusSq = radius * radius; 97 | const rayToCenter = vec3_sub( [0,0,0], origin, ray.posStart ); 98 | const tProj = vec3_dot( rayToCenter, ray.direction ); // Project the length to the center onto the Ray 99 | 100 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | // Get length of projection point to center and check if its within the sphere 102 | // Opposite^2 = hyptenuse^2 - adjacent^2 103 | const oppLenSq = vec3_sqrLen( rayToCenter ) - ( tProj * tProj ); 104 | return !( oppLenSq > radiusSq ); 105 | } 106 | 107 | /** T returned is scale to vector length, not direction */ 108 | export function intersectPlane( ray, planePos, planeNorm ){ 109 | // ((planePos - rayOrigin) dot planeNorm) / ( rayVecLen dot planeNorm ) 110 | // pos = t * rayVecLen + rayOrigin; 111 | const denom = vec3_dot( ray.vecLength, planeNorm ); // Dot product of ray Length and plane normal 112 | if( denom <= 0.000001 && denom >= -0.000001 ) return null; // abs(denom) < epsilon, using && instead to not perform absolute. 113 | 114 | const t = vec3_dot( vec3_sub( [0,0,0], planePos, ray.posStart ), planeNorm ) / denom; 115 | return ( t >= 0 )? t : null; 116 | } 117 | 118 | export function intersectTri( ray, v0, v1, v2, out, cullFace=true ){ 119 | const v0v1 = vec3_sub( [0,0,0], v1, v0 ); 120 | const v0v2 = vec3_sub( [0,0,0], v2, v0 ); 121 | const pvec = vec3_cross( [0,0,0], ray.direction, v0v2 ); 122 | const det = vec3_dot( v0v1, pvec ); 123 | 124 | if( cullFace && det < 0.000001 ) return false; 125 | 126 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 127 | const idet = 1 / det; 128 | const tvec = vec3_sub( [0,0,0], ray.posStart, v0 ); 129 | const u = vec3_dot( tvec, pvec ) * idet; 130 | 131 | if( u < 0 || u > 1 ) return false; 132 | 133 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | const qvec = vec3_cross( [0,0,0], tvec, v0v1, ); 135 | const v = vec3_dot( ray.direction, qvec ) * idet; 136 | 137 | if( v < 0 || u+v > 1 ) return false; 138 | 139 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 140 | if( out ){ 141 | const len = vec3_dot( v0v2, qvec ) * idet; 142 | ray.directionAt( len, out ); 143 | } 144 | 145 | return true; 146 | } 147 | 148 | export function nearPoint( ray, p, distLimit=0.1 ){ 149 | /* closest_point_to_line3D 150 | let dx = bx - ax, 151 | dy = by - ay, 152 | dz = bz - az, 153 | t = ( (px-ax)*dx + (py-ay)*dy + (pz-az)*dz ) / ( dx*dx + dy*dy + dz*dz ) ; */ 154 | const v = vec3_sub( [0,0,0], p, ray.posStart ); 155 | vec3_mul( v, v, ray.vecLength ); 156 | 157 | const t = ( v[0] + v[1] + v[2] ) / vec3_sqrLen( ray.vecLength ); 158 | 159 | if( t < 0 || t > 1 ) return null; // Over / Under shoots the Ray Segment 160 | const lenSqr = vec3_sqrLen( ray.posAt( t, v ), p ); // Distance from point to nearest point on ray. 161 | 162 | return ( lenSqr <= (distLimit*distLimit) )? t : null; 163 | } 164 | 165 | export class NearSegmentResult{ 166 | segPosition = [0,0,0]; 167 | rayPosition = [0,0,0]; 168 | distanceSq = 0; 169 | distance = 0; 170 | } 171 | 172 | /** Returns [ T of Segment, T of RayLen ] */ 173 | export function nearSegment( ray, p0, p1, results=null){ 174 | // http://geomalgorithms.com/a07-_distance.html 175 | const u = vec3_sub( [0,0,0], p1, p0 ), 176 | v = ray.vecLength, 177 | w = vec3_sub( [0,0,0], p0, ray.posStart ), 178 | a = vec3_dot( u, u ), // always >= 0 179 | b = vec3_dot( u, v ), 180 | c = vec3_dot( v, v ), // always >= 0 181 | d = vec3_dot( u, w ), 182 | e = vec3_dot( v, w ), 183 | D = a * c - b * b; // always >= 0 184 | 185 | let tU = 0, // T Of Segment 186 | tV = 0; // T Of Ray 187 | 188 | // Compute the line parameters of the two closest points 189 | if( D < 0.000001 ){ // the lines are almost parallel 190 | tU = 0.0; 191 | tV = ( b > c ? d/b : e/c ); // use the largest denominator 192 | }else{ 193 | tU = ( b*e - c*d ) / D; 194 | tV = ( a*e - b*d ) / D; 195 | } 196 | 197 | if( tU < 0 || tU > 1 || tV < 0 || tV > 1 ) return false; 198 | 199 | // Segment Position : u.scale( tU ).add( p0 ) 200 | // Ray Position : v.scale( tV ).add( this.origin ) ]; 201 | if( results ){ 202 | vec3_lerp( results.rayPosition, ray.posStart, ray.posEnd, tV ); 203 | vec3_lerp( results.segPosition, p0, p1, tU ); 204 | results.distance = vec3_len( results.segPosition, results.rayPosition ); 205 | } 206 | 207 | return true; 208 | } -------------------------------------------------------------------------------- /src3/lib/RayIntersection.js: -------------------------------------------------------------------------------- 1 | // #region IMPORT 2 | import { 3 | vec3_add, 4 | vec3_sub, 5 | vec3_mul, 6 | vec3_norm, 7 | vec3_copy, 8 | vec3_scale, 9 | vec3_dot, 10 | vec3_cross, 11 | vec3_sqrLen, 12 | vec3_lerp, 13 | vec3_len, 14 | vec4_transformMat4, 15 | mat4_invert, 16 | mat4_mul, 17 | } from './Maths.js'; 18 | // #endregion 19 | 20 | export class Ray{ 21 | posStart = [0,0,0]; // Origin 22 | posEnd = [0,0,0]; 23 | direction = [0,0,0]; // Direction from Start to End 24 | vecLength = [0,0,0]; // Vector Length between start to end 25 | 26 | // #region GETTERS / SETTERS 27 | /** Get position of the ray from T Scale of VecLen */ 28 | posAt( t, out){ 29 | // RayVecLen * t + RayOrigin 30 | // also works lerp( RayOrigin, RayEnd, t ) 31 | out = out || [0,0,0]; 32 | out[ 0 ] = this.vecLength[ 0 ] * t + this.posStart[ 0 ]; 33 | out[ 1 ] = this.vecLength[ 1 ] * t + this.posStart[ 1 ]; 34 | out[ 2 ] = this.vecLength[ 2 ] * t + this.posStart[ 2 ]; 35 | return out; 36 | } 37 | 38 | /** Get position of the ray from distance from origin */ 39 | directionAt( len, out ){ 40 | out = out || [0,0,0]; 41 | out[ 0 ] = this.direction[ 0 ] * len + this.posStart[ 0 ]; 42 | out[ 1 ] = this.direction[ 1 ] * len + this.posStart[ 1 ]; 43 | out[ 2 ] = this.direction[ 2 ] * len + this.posStart[ 2 ]; 44 | return out; 45 | } 46 | 47 | fromCaster( caster ){ 48 | vec3_copy( this.posStart, caster.ray.origin.toArray() ); 49 | vec3_copy( this.direction, caster.ray.direction.toArray() ); 50 | 51 | const len = ( caster.far == Infinity )? 1000 : caster.far; 52 | vec3_scale( this.vecLength, this.direction, len ); 53 | vec3_add( this.posEnd, this.posStart, this.vecLength ); 54 | } 55 | 56 | fromScreenProjection( x, y, w, h, projMatrix, camMatrix ){ 57 | // http://antongerdelan.net/opengl/raycasting.html 58 | // Normalize Device Coordinate 59 | const nx = x / w * 2 - 1; 60 | const ny = 1 - y / h * 2; 61 | 62 | // inverseWorldMatrix = invert( ProjectionMatrix * ViewMatrix ) OR 63 | // inverseWorldMatrix = localMatrix * invert( ProjectionMatrix ) 64 | const invMatrix = [ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 ]; 65 | mat4_invert( invMatrix, projMatrix ) 66 | mat4_mul( invMatrix, camMatrix, invMatrix ); 67 | 68 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | // https://stackoverflow.com/questions/20140711/picking-in-3d-with-ray-tracing-using-ninevehgl-or-opengl-i-phone/20143963#20143963 70 | // Clip Cords would be [nx,ny,-1,1]; 71 | const clipNear = [ nx, ny, -1, 1 ]; 72 | const clipFar = [ nx, ny, 1, 1 ]; 73 | 74 | // using 4d Homogeneous Clip Coordinates 75 | vec4_transformMat4( clipNear, clipNear, invMatrix ); 76 | vec4_transformMat4( clipFar, clipFar, invMatrix ); 77 | 78 | // Normalize by using W component 79 | for( let i=0; i < 3; i++){ 80 | clipNear[ i ] /= clipNear[ 3 ]; 81 | clipFar [ i ] /= clipFar [ 3 ]; 82 | } 83 | 84 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | vec3_copy( this.posStart, clipNear ); // Starting Point of the Ray 86 | vec3_copy( this.posEnd, clipFar ); // The absolute end of the ray 87 | vec3_sub( this.vecLength, clipFar, clipNear ); // Vector Length 88 | vec3_norm( this.direction, this.vecLength ); // Normalized Vector Length 89 | return this; 90 | } 91 | // #endregion 92 | } 93 | 94 | export function intersectSphere( ray, origin, radius ){ 95 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 96 | const radiusSq = radius * radius; 97 | const rayToCenter = vec3_sub( [0,0,0], origin, ray.posStart ); 98 | const tProj = vec3_dot( rayToCenter, ray.direction ); // Project the length to the center onto the Ray 99 | 100 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | // Get length of projection point to center and check if its within the sphere 102 | // Opposite^2 = hyptenuse^2 - adjacent^2 103 | const oppLenSq = vec3_sqrLen( rayToCenter ) - ( tProj * tProj ); 104 | return !( oppLenSq > radiusSq ); 105 | } 106 | 107 | /** T returned is scale to vector length, not direction */ 108 | export function intersectPlane( ray, planePos, planeNorm ){ 109 | // ((planePos - rayOrigin) dot planeNorm) / ( rayVecLen dot planeNorm ) 110 | // pos = t * rayVecLen + rayOrigin; 111 | const denom = vec3_dot( ray.vecLength, planeNorm ); // Dot product of ray Length and plane normal 112 | if( denom <= 0.000001 && denom >= -0.000001 ) return null; // abs(denom) < epsilon, using && instead to not perform absolute. 113 | 114 | const t = vec3_dot( vec3_sub( [0,0,0], planePos, ray.posStart ), planeNorm ) / denom; 115 | return ( t >= 0 )? t : null; 116 | } 117 | 118 | export function intersectTri( ray, v0, v1, v2, out, cullFace=true ){ 119 | const v0v1 = vec3_sub( [0,0,0], v1, v0 ); 120 | const v0v2 = vec3_sub( [0,0,0], v2, v0 ); 121 | const pvec = vec3_cross( [0,0,0], ray.direction, v0v2 ); 122 | const det = vec3_dot( v0v1, pvec ); 123 | 124 | if( cullFace && det < 0.000001 ) return false; 125 | 126 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 127 | const idet = 1 / det; 128 | const tvec = vec3_sub( [0,0,0], ray.posStart, v0 ); 129 | const u = vec3_dot( tvec, pvec ) * idet; 130 | 131 | if( u < 0 || u > 1 ) return false; 132 | 133 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | const qvec = vec3_cross( [0,0,0], tvec, v0v1, ); 135 | const v = vec3_dot( ray.direction, qvec ) * idet; 136 | 137 | if( v < 0 || u+v > 1 ) return false; 138 | 139 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 140 | if( out ){ 141 | const len = vec3_dot( v0v2, qvec ) * idet; 142 | ray.directionAt( len, out ); 143 | } 144 | 145 | return true; 146 | } 147 | 148 | export function nearPoint( ray, p, distLimit=0.1 ){ 149 | /* closest_point_to_line3D 150 | let dx = bx - ax, 151 | dy = by - ay, 152 | dz = bz - az, 153 | t = ( (px-ax)*dx + (py-ay)*dy + (pz-az)*dz ) / ( dx*dx + dy*dy + dz*dz ) ; */ 154 | const v = vec3_sub( [0,0,0], p, ray.posStart ); 155 | vec3_mul( v, v, ray.vecLength ); 156 | 157 | const t = ( v[0] + v[1] + v[2] ) / vec3_sqrLen( ray.vecLength ); 158 | 159 | if( t < 0 || t > 1 ) return null; // Over / Under shoots the Ray Segment 160 | const lenSqr = vec3_sqrLen( ray.posAt( t, v ), p ); // Distance from point to nearest point on ray. 161 | 162 | return ( lenSqr <= (distLimit*distLimit) )? t : null; 163 | } 164 | 165 | export class NearSegmentResult{ 166 | segPosition = [0,0,0]; 167 | rayPosition = [0,0,0]; 168 | distanceSq = 0; 169 | distance = 0; 170 | } 171 | 172 | /** Returns [ T of Segment, T of RayLen ] */ 173 | export function nearSegment( ray, p0, p1, results=null){ 174 | // http://geomalgorithms.com/a07-_distance.html 175 | const u = vec3_sub( [0,0,0], p1, p0 ), 176 | v = ray.vecLength, 177 | w = vec3_sub( [0,0,0], p0, ray.posStart ), 178 | a = vec3_dot( u, u ), // always >= 0 179 | b = vec3_dot( u, v ), 180 | c = vec3_dot( v, v ), // always >= 0 181 | d = vec3_dot( u, w ), 182 | e = vec3_dot( v, w ), 183 | D = a * c - b * b; // always >= 0 184 | 185 | let tU = 0, // T Of Segment 186 | tV = 0; // T Of Ray 187 | 188 | // Compute the line parameters of the two closest points 189 | if( D < 0.000001 ){ // the lines are almost parallel 190 | tU = 0.0; 191 | tV = ( b > c ? d/b : e/c ); // use the largest denominator 192 | }else{ 193 | tU = ( b*e - c*d ) / D; 194 | tV = ( a*e - b*d ) / D; 195 | } 196 | 197 | if( tU < 0 || tU > 1 || tV < 0 || tV > 1 ) return false; 198 | 199 | // Segment Position : u.scale( tU ).add( p0 ) 200 | // Ray Position : v.scale( tV ).add( this.origin ) ]; 201 | if( results ){ 202 | vec3_lerp( results.rayPosition, ray.posStart, ray.posEnd, tV ); 203 | vec3_lerp( results.segPosition, p0, p1, tU ); 204 | results.distance = vec3_len( results.segPosition, results.rayPosition ); 205 | } 206 | 207 | return true; 208 | } -------------------------------------------------------------------------------- /src/ManipulatorMesh.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Group, Line, Mesh, DoubleSide, 4 | MeshBasicMaterial, LineBasicMaterial, 5 | BufferGeometry, Float32BufferAttribute, 6 | SphereGeometry, TorusGeometry, CylinderGeometry, 7 | } from 'three'; 8 | import { ManipulatorMode } from './ManipulatorData.js'; 9 | 10 | export class ManipulatorMesh extends Group{ 11 | // #region MAIN 12 | axisColors = [ 0x81D773, 0x6DA9EA, 0xF7716A ]; 13 | axisLines = []; 14 | axisArcs = []; 15 | axisPoints = []; 16 | axisTris = []; 17 | 18 | grpCtrl = new Group(); 19 | meshTracePnt = null; 20 | meshTraceLine = null; 21 | 22 | colSelect = 0xffffff; 23 | colOrigin = 0xffff00; 24 | 25 | constructor( data ){ 26 | super(); 27 | const PIH = Math.PI * 0.5; 28 | const lineRadius = 0.03; 29 | const arcRadius = data.arcRadius; 30 | const arcThickness = 0.03 31 | const sclDistance = data.sclPointLen; 32 | 33 | this.visible = false; 34 | 35 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | // MATERIALS 37 | const matBasic = new MeshBasicMaterial( { 38 | depthTest: false, 39 | depthWrite: false, 40 | fog: false, 41 | toneMapped: false, 42 | transparent: true, 43 | side: DoubleSide, 44 | opacity: 1.0, 45 | color: 0xffffff, 46 | } ); 47 | 48 | const matLine = new LineBasicMaterial( { 49 | depthTest: false, 50 | depthWrite: false, 51 | fog: false, 52 | toneMapped: false, 53 | transparent: true, 54 | color: 0x909090, 55 | } ); 56 | 57 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | // GEOMETRY 59 | const geoTrace = new BufferGeometry(); 60 | geoTrace.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 100, 0 ], 3 ) ); 61 | 62 | const geoTri = new BufferGeometry(); 63 | geoTri.setAttribute( 'position', new Float32BufferAttribute( [ 0,0,0, data.midPointLen,0,0, 0,data.midPointLen,0 ], 3 ) ); 64 | 65 | const geoSphere = new SphereGeometry( 0.1, 8, 8 ); 66 | const geoArc = new TorusGeometry( arcRadius, arcThickness, 3, 10, PIH ); 67 | const geoAxisLine = new CylinderGeometry( lineRadius, lineRadius, data.axisLen, 3 ); 68 | geoAxisLine.translate( 0, data.axisLen * 0.5, 0 ); 69 | 70 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | // AXIS LINES 72 | const yAxisLine = new Mesh( geoAxisLine, matBasic.clone() ); 73 | this.grpCtrl.add( yAxisLine ); 74 | 75 | const zAxisLine = new Mesh( geoAxisLine, matBasic.clone() ); 76 | zAxisLine.rotation.x = PIH; 77 | this.grpCtrl.add( zAxisLine ); 78 | 79 | const xAxisLine = new Mesh( geoAxisLine, matBasic.clone() ); 80 | xAxisLine.rotation.z = -PIH; 81 | this.grpCtrl.add( xAxisLine ); 82 | 83 | this.axisLines.push( xAxisLine, yAxisLine, zAxisLine ); 84 | 85 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 86 | // AXIS LINES 87 | const zAxisArc = new Mesh( geoArc, matBasic.clone() ); 88 | this.grpCtrl.add( zAxisArc ); 89 | 90 | const xAxisArc = new Mesh( geoArc, matBasic.clone() ); 91 | xAxisArc.rotation.y = -PIH; 92 | this.grpCtrl.add( xAxisArc ); 93 | 94 | const yAxisArc = new Mesh( geoArc, matBasic.clone() ); 95 | yAxisArc.rotation.x = PIH; 96 | this.grpCtrl.add( yAxisArc ); 97 | 98 | this.axisArcs.push( xAxisArc, yAxisArc, zAxisArc ); 99 | 100 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | // SCALE SELECTORS 102 | const zAxisPnt = new Mesh( geoSphere, matBasic.clone() ); 103 | zAxisPnt.position.z = sclDistance; 104 | this.grpCtrl.add( zAxisPnt ); 105 | 106 | const xAxisPnt = new Mesh( geoSphere, matBasic.clone() ); 107 | xAxisPnt.position.x = sclDistance; 108 | this.grpCtrl.add( xAxisPnt ); 109 | 110 | const yAxisPnt = new Mesh( geoSphere, matBasic.clone() ); 111 | yAxisPnt.position.y = sclDistance; 112 | this.grpCtrl.add( yAxisPnt ); 113 | 114 | this.axisPoints.push( xAxisPnt, yAxisPnt, zAxisPnt ); 115 | 116 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 117 | // PLANE SELECTORS 118 | const zAxisTri = new Mesh( geoTri, matBasic.clone() ); 119 | this.grpCtrl.add( zAxisTri ); 120 | 121 | const yAxisTri = new Mesh( geoTri, matBasic.clone() ); 122 | yAxisTri.rotation.x = PIH; 123 | this.grpCtrl.add( yAxisTri ); 124 | 125 | const xAxisTri = new Mesh( geoTri, matBasic.clone() ); 126 | xAxisTri.rotation.y = -PIH; 127 | this.grpCtrl.add( xAxisTri ); 128 | 129 | this.axisTris.push( xAxisTri, yAxisTri, zAxisTri ); 130 | 131 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 132 | 133 | this.meshTraceLine = new Line( geoTrace, matLine ); 134 | this.meshTraceLine.visible = false; 135 | this.add( this.meshTraceLine ); 136 | 137 | this.meshTracePnt = new Mesh( geoSphere, matBasic.clone() ); 138 | this.meshTracePnt.visible = false; 139 | this.add( this.meshTracePnt ); 140 | 141 | this.origin = new Mesh( geoSphere, matBasic.clone() ); 142 | this.grpCtrl.add( this.origin ); 143 | this.add( this.grpCtrl ); 144 | } 145 | 146 | // #endregion 147 | 148 | showGizmo(){ this.grpCtrl.visible = true; } 149 | hideGizmo(){ this.grpCtrl.visible = false; } 150 | 151 | // Update the visible state of the gizmo parts 152 | updateLook( data ){ 153 | let itm; 154 | for( itm of this.axisArcs ) itm.visible = data.useRotate; 155 | 156 | this.origin.visible = data.useScale; 157 | for( itm of this.axisPoints ) itm.visible = data.useScale; 158 | 159 | for( itm of this.axisTris ) itm.visible = data.useTranslate; 160 | } 161 | 162 | update( data ){ 163 | if( !data.hasUpdated && !data.hasHit ) return; 164 | 165 | this.grpCtrl.scale.fromArray( data.scale ); 166 | this.grpCtrl.position.fromArray( data.position ); 167 | 168 | if( data.activeAxis === -2 && data.activeMode === ManipulatorMode.Scale ){ 169 | this.origin.material.color.setHex( this.colSelect ); 170 | }else{ 171 | this.origin.material.color.setHex( this.colOrigin ); 172 | } 173 | 174 | for( let i=0; i < 3; i++ ){ 175 | this.axisLines[ i ].material.color.setHex( this.axisColors[ i ] ); 176 | this.axisArcs[ i ].material.color.setHex( this.axisColors[ i ] ); 177 | this.axisPoints[ i ].material.color.setHex( this.axisColors[ i ] ); 178 | this.axisTris[ i ].material.color.setHex( this.axisColors[ i ] ); 179 | 180 | if( i === data.activeAxis ){ 181 | switch( data.activeMode ){ 182 | case ManipulatorMode.Translate: this.axisLines[ i ].material.color.setHex( this.colSelect ); break; 183 | case ManipulatorMode.Rotate: this.axisArcs[ i ].material.color.setHex( this.colSelect ); break; 184 | case ManipulatorMode.Scale: this.axisPoints[ i ].material.color.setHex( this.colSelect ); break; 185 | } 186 | } 187 | 188 | if( i === data.activePlane ){ 189 | this.axisTris[ i ].material.color.setHex( 0xffffff ); 190 | } 191 | } 192 | 193 | 194 | if( data.traceLine.isActive ){ 195 | const sclPnt = Math.abs( data.scale[2] ); 196 | this.meshTracePnt.visible = true; 197 | this.meshTracePnt.scale.set( sclPnt, sclPnt, sclPnt ); 198 | this.meshTracePnt.position.fromArray( data.traceLine.origin ); 199 | 200 | this.meshTraceLine.visible = true; 201 | this.meshTraceLine.geometry.attributes.position.needsUpdate = true; 202 | 203 | const pntArray = this.meshTraceLine.geometry.attributes.position.array; 204 | pntArray[ 0 ] = data.traceLine.a[ 0 ]; 205 | pntArray[ 1 ] = data.traceLine.a[ 1 ]; 206 | pntArray[ 2 ] = data.traceLine.a[ 2 ]; 207 | pntArray[ 3 ] = data.traceLine.b[ 0 ]; 208 | pntArray[ 4 ] = data.traceLine.b[ 1 ]; 209 | pntArray[ 5 ] = data.traceLine.b[ 2 ]; 210 | }else{ 211 | this.meshTraceLine.visible = false; 212 | this.meshTracePnt.visible = false; 213 | } 214 | } 215 | } -------------------------------------------------------------------------------- /src/Maths.js: -------------------------------------------------------------------------------- 1 | // #region VEC3 2 | export function vec3_copy( out, a ){ 3 | out[0] = a[0]; 4 | out[1] = a[1]; 5 | out[2] = a[2]; 6 | return out; 7 | } 8 | 9 | export function vec3_add( out, a, b ){ 10 | out[ 0 ] = a[ 0 ] + b[ 0 ]; 11 | out[ 1 ] = a[ 1 ] + b[ 1 ]; 12 | out[ 2 ] = a[ 2 ] + b[ 2 ]; 13 | return out; 14 | } 15 | 16 | export function vec3_sub( out, a, b ){ 17 | out[ 0 ] = a[ 0 ] - b[ 0 ]; 18 | out[ 1 ] = a[ 1 ] - b[ 1 ]; 19 | out[ 2 ] = a[ 2 ] - b[ 2 ]; 20 | return out; 21 | } 22 | 23 | export function vec3_cross( out, a, b ){ 24 | const ax = a[0], ay = a[1], az = a[2], 25 | bx = b[0], by = b[1], bz = b[2]; 26 | 27 | out[ 0 ] = ay * bz - az * by; 28 | out[ 1 ] = az * bx - ax * bz; 29 | out[ 2 ] = ax * by - ay * bx; 30 | return out; 31 | } 32 | 33 | export function vec3_dot( a, b ){ return a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] + a[ 2 ] * b[ 2 ]; } 34 | 35 | export function vec3_scaleAndAdd( out, add, v, s ){ 36 | out[ 0 ] = v[ 0 ] * s + add[ 0 ]; 37 | out[ 1 ] = v[ 1 ] * s + add[ 1 ]; 38 | out[ 2 ] = v[ 2 ] * s + add[ 2 ]; 39 | return out; 40 | } 41 | 42 | export function vec3_scale( out, a, s ){ 43 | out[ 0 ] = a[ 0 ] * s; 44 | out[ 1 ] = a[ 1 ] * s; 45 | out[ 2 ] = a[ 2 ] * s; 46 | return out; 47 | } 48 | 49 | export function vec3_norm( out, a){ 50 | let mag = Math.sqrt( a[ 0 ]**2 + a[ 1 ]**2 + a[ 2 ]**2 ); 51 | if( mag != 0 ){ 52 | mag = 1 / mag; 53 | out[ 0 ] = a[ 0 ] * mag; 54 | out[ 1 ] = a[ 1 ] * mag; 55 | out[ 2 ] = a[ 2 ] * mag; 56 | } 57 | return out; 58 | } 59 | 60 | export function vec3_transformQuat( out, v, q ){ 61 | const qx = q[0], qy = q[1], qz = q[2], qw = q[3], 62 | vx = v[0], vy = v[1], vz = v[2], 63 | x1 = qy * vz - qz * vy, 64 | y1 = qz * vx - qx * vz, 65 | z1 = qx * vy - qy * vx, 66 | x2 = qw * x1 + qy * z1 - qz * y1, 67 | y2 = qw * y1 + qz * x1 - qx * z1, 68 | z2 = qw * z1 + qx * y1 - qy * x1; 69 | out[ 0 ] = vx + 2 * x2; 70 | out[ 1 ] = vy + 2 * y2; 71 | out[ 2 ] = vz + 2 * z2; 72 | return out; 73 | } 74 | 75 | export function vec3_transformMat4(out, a, m) { 76 | let x = a[0], y = a[1], z = a[2]; 77 | let w = ( m[3] * x + m[7] * y + m[11] * z + m[15] ) || 1.0; 78 | out[0] = ( m[0] * x + m[4] * y + m[8] * z + m[12] ) / w; 79 | out[1] = ( m[1] * x + m[5] * y + m[9] * z + m[13] ) / w; 80 | out[2] = ( m[2] * x + m[6] * y + m[10] * z + m[14] ) / w; 81 | return out; 82 | } 83 | 84 | export function vec3_sqrLen( a, b ){ 85 | if( b === undefined ) return a[ 0 ]**2 + a[ 1 ]**2 + a[ 2 ]** 2; 86 | return (a[ 0 ]-b[ 0 ]) ** 2 + (a[ 1 ]-b[ 1 ]) ** 2 + (a[ 2 ]-b[ 2 ]) ** 2; 87 | } 88 | 89 | export function vec3_len( a, b ){ 90 | if( b === undefined ) return Math.sqrt( a[ 0 ]**2 + a[ 1 ]**2 + a[ 2 ]** 2 ); 91 | return Math.sqrt( (a[ 0 ]-b[ 0 ]) ** 2 + (a[ 1 ]-b[ 1 ]) ** 2 + (a[ 2 ]-b[ 2 ]) ** 2 ); 92 | } 93 | 94 | export function vec3_mul( out, a, b ){ 95 | out[ 0 ] = a[ 0 ] * b[ 0 ]; 96 | out[ 1 ] = a[ 1 ] * b[ 1 ]; 97 | out[ 2 ] = a[ 2 ] * b[ 2 ]; 98 | return out; 99 | } 100 | 101 | export function vec3_lerp( out, a, b, t ) { 102 | const ti = 1 - t; // Linear Interpolation : (1 - t) * v0 + t * v1; 103 | out[ 0 ] = a[ 0 ] * ti + b[ 0 ] * t; 104 | out[ 1 ] = a[ 1 ] * ti + b[ 1 ] * t; 105 | out[ 2 ] = a[ 2 ] * ti + b[ 2 ] * t; 106 | return out; 107 | } 108 | // #endregion 109 | 110 | // #region QUAT 111 | export function quat_copy( out, a ) { 112 | out[ 0 ] = a[ 0 ]; 113 | out[ 1 ] = a[ 1 ]; 114 | out[ 2 ] = a[ 2 ]; 115 | out[ 3 ] = a[ 3 ]; 116 | return out; 117 | } 118 | 119 | export function quat_mul( out, a, b ) { 120 | const ax = a[0], ay = a[1], az = a[2], aw = a[3], 121 | bx = b[0], by = b[1], bz = b[2], bw = b[3]; 122 | out[ 0 ] = ax * bw + aw * bx + ay * bz - az * by; 123 | out[ 1 ] = ay * bw + aw * by + az * bx - ax * bz; 124 | out[ 2 ] = az * bw + aw * bz + ax * by - ay * bx; 125 | out[ 3 ] = aw * bw - ax * bx - ay * by - az * bz; 126 | return out; 127 | } 128 | 129 | export function quat_normalize( out, q ){ 130 | let len = q[0]**2 + q[1]**2 + q[2]**2 + q[3]**2; 131 | if( len > 0 ){ 132 | len = 1 / Math.sqrt( len ); 133 | out[ 0 ] = q[ 0 ] * len; 134 | out[ 1 ] = q[ 1 ] * len; 135 | out[ 2 ] = q[ 2 ] * len; 136 | out[ 3 ] = q[ 3 ] * len; 137 | } 138 | return out; 139 | } 140 | 141 | export function quat_setAxisAngle( out, axis, rad ){ 142 | const half = rad * .5, 143 | s = Math.sin( half ); 144 | out[ 0 ] = s * axis[ 0 ]; 145 | out[ 1 ] = s * axis[ 1 ]; 146 | out[ 2 ] = s * axis[ 2 ]; 147 | out[ 3 ] = Math.cos( half ); 148 | return out; 149 | } 150 | 151 | export function quat_sqrLen( a, b ){ 152 | if( b === undefined ) return a[ 0 ]**2 + a[ 1 ]**2 + a[ 2 ]** 2 + a[ 3 ]** 2; 153 | return (a[ 0 ]-b[ 0 ]) ** 2 + (a[ 1 ]-b[ 1 ]) ** 2 + (a[ 2 ]-b[ 2 ]) ** 2 + (a[ 3 ]-b[ 3 ]) ** 2; 154 | } 155 | 156 | // #endregion 157 | 158 | // #region MAT4 159 | 160 | export function mat4_invert(out, a) { 161 | let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; 162 | let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; 163 | let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; 164 | let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; 165 | 166 | let b00 = a00 * a11 - a01 * a10; 167 | let b01 = a00 * a12 - a02 * a10; 168 | let b02 = a00 * a13 - a03 * a10; 169 | let b03 = a01 * a12 - a02 * a11; 170 | let b04 = a01 * a13 - a03 * a11; 171 | let b05 = a02 * a13 - a03 * a12; 172 | let b06 = a20 * a31 - a21 * a30; 173 | let b07 = a20 * a32 - a22 * a30; 174 | let b08 = a20 * a33 - a23 * a30; 175 | let b09 = a21 * a32 - a22 * a31; 176 | let b10 = a21 * a33 - a23 * a31; 177 | let b11 = a22 * a33 - a23 * a32; 178 | 179 | // Calculate the determinant 180 | let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 181 | 182 | if (!det) return null; 183 | det = 1.0 / det; 184 | 185 | out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; 186 | out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; 187 | out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; 188 | out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; 189 | out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; 190 | out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; 191 | out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; 192 | out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; 193 | out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; 194 | out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; 195 | out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; 196 | out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; 197 | out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; 198 | out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; 199 | out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; 200 | out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; 201 | 202 | return out; 203 | } 204 | 205 | export function mat4_mul( out, a, b ){ 206 | let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; 207 | let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; 208 | let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; 209 | let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; 210 | 211 | // Cache only the current line of the second matrix 212 | let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; 213 | out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 214 | out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 215 | out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 216 | out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 217 | 218 | b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; 219 | out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 220 | out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 221 | out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 222 | out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 223 | 224 | b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; 225 | out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 226 | out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 227 | out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 228 | out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 229 | 230 | b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; 231 | out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 232 | out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 233 | out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 234 | out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 235 | return out; 236 | } 237 | // #endregion 238 | 239 | // #region VEC4 240 | export function vec4_transformMat4( out, a, m ){ 241 | let x = a[0], y = a[1], z = a[2], w = a[3]; 242 | out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; 243 | out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; 244 | out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; 245 | out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; 246 | return out; 247 | } 248 | // #endregion -------------------------------------------------------------------------------- /test/_lib/DynLineMesh.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | // X and Y axis need to be normalized vectors, 90 degrees of eachother. 4 | function planeCircle( vecCenter, xAxis, yAxis, angle, radius, out ){ 5 | const sin = Math.sin(angle); 6 | const cos = Math.cos(angle); 7 | out[0] = vecCenter[0] + radius * cos * xAxis[0] + radius * sin * yAxis[0]; 8 | out[1] = vecCenter[1] + radius * cos * xAxis[1] + radius * sin * yAxis[1]; 9 | out[2] = vecCenter[2] + radius * cos * xAxis[2] + radius * sin * yAxis[2]; 10 | return out; 11 | } 12 | 13 | 14 | class DynLineMesh extends THREE.LineSegments{ 15 | _defaultColor = 0x00ff00; 16 | _cnt = 0; 17 | _verts = []; 18 | _color = []; 19 | _config = []; 20 | _dirty = false; 21 | 22 | constructor( initSize = 20 ){ 23 | super( 24 | _newDynLineMeshGeometry( 25 | new Float32Array( initSize * 2 * 3 ), // Two Points for Each Line 26 | new Float32Array( initSize * 2 * 3 ), 27 | new Float32Array( initSize * 2 * 1 ), 28 | false 29 | ), 30 | newDynLineMeshMaterial() //new THREE.PointsMaterial( { color: 0xffffff, size:8, sizeAttenuation:false } ) 31 | ); 32 | 33 | this.geometry.setDrawRange( 0, 0 ); 34 | this.onBeforeRender = ()=>{ if( this._dirty ) this._updateGeometry(); } 35 | } 36 | 37 | useDepth( b ){ this.material.depthTest = b; return this; } 38 | 39 | reset(){ 40 | this._cnt = 0; 41 | this._verts.length = 0; 42 | this._color.length = 0; 43 | this._config.length = 0; 44 | this.geometry.setDrawRange( 0, 0 ); 45 | return this; 46 | } 47 | 48 | add( p0, p1, color0=this._defaultColor, color1=null, isDash=false ){ 49 | this._verts.push( p0[0], p0[1], p0[2], p1[0], p1[1], p1[2] ); 50 | this._color.push( ...glColor( color0 ), ...glColor( (color1 != null) ? color1:color0 ) ); 51 | 52 | if( isDash ){ 53 | const len = Math.sqrt( 54 | (p1[0] - p0[0]) ** 2 + 55 | (p1[1] - p0[1]) ** 2 + 56 | (p1[2] - p0[2]) ** 2 57 | ); 58 | this._config.push( 0, len ); 59 | }else{ 60 | this._config.push( 0, 0 ); 61 | } 62 | 63 | this._cnt++; 64 | this._dirty = true; 65 | return this; 66 | } 67 | 68 | box( v0, v1, col=ln_color, is_dash=false ){ 69 | let x1 = v0[0], y1 = v0[1], z1 = v0[2], 70 | x2 = v1[0], y2 = v1[1], z2 = v1[2]; 71 | 72 | this.add( [x1,y1,z1], [x1,y1,z2], col, null, is_dash ); // Bottom 73 | this.add( [x1,y1,z2], [x2,y1,z2], col, null, is_dash ); 74 | this.add( [x2,y1,z2], [x2,y1,z1], col, null, is_dash ); 75 | this.add( [x2,y1,z1], [x1,y1,z1], col, null, is_dash ); 76 | this.add( [x1,y2,z1], [x1,y2,z2], col, null, is_dash ); // Top 77 | this.add( [x1,y2,z2], [x2,y2,z2], col, null, is_dash ); 78 | this.add( [x2,y2,z2], [x2,y2,z1], col, null, is_dash ); 79 | this.add( [x2,y2,z1], [x1,y2,z1], col, null, is_dash ); 80 | this.add( [x1,y1,z1], [x1,y2,z1], col, null, is_dash ); // Sides 81 | this.add( [x1,y1,z2], [x1,y2,z2], col, null, is_dash ); 82 | this.add( [x2,y1,z2], [x2,y2,z2], col, null, is_dash ); 83 | this.add( [x2,y1,z1], [x2,y2,z1], col, null, is_dash ); 84 | return this; 85 | } 86 | 87 | circle( origin, xAxis, yAxis, radius, seg, col=this._defaultColor, is_dash=false ){ 88 | const prevPos = [0,0,0]; 89 | const pos = [0,0,0]; 90 | const PI2 = Math.PI * 2; 91 | let rad = 0; 92 | 93 | planeCircle( origin, xAxis, yAxis, 0, radius, prevPos ); 94 | for( let i=1; i <= seg; i++ ){ 95 | rad = PI2 * ( i / seg ); 96 | planeCircle( origin, xAxis, yAxis, rad, radius, pos ); 97 | this.add( prevPos, pos, col, null, is_dash ); 98 | 99 | prevPos[0] = pos[0]; 100 | prevPos[1] = pos[1]; 101 | prevPos[2] = pos[2]; 102 | } 103 | } 104 | 105 | arc( origin, xAxis, yAxis, radius, seg, arcRad, initRad=0, col=this._defaultColor, is_dash=false ){ 106 | const prevPos = [0,0,0]; 107 | const pos = [0,0,0]; 108 | const PI2 = Math.PI * 2; 109 | let rad = 0; 110 | 111 | planeCircle( origin, xAxis, yAxis, 0, radius, prevPos ); 112 | for( let i=1; i <= seg; i++ ){ 113 | rad = arcRad * ( i / seg ) + initRad; 114 | planeCircle( origin, xAxis, yAxis, rad, radius, pos ); 115 | this.add( prevPos, pos, col, null, is_dash ); 116 | 117 | prevPos[0] = pos[0]; 118 | prevPos[1] = pos[1]; 119 | prevPos[2] = pos[2]; 120 | } 121 | } 122 | 123 | _updateGeometry(){ 124 | const geo = this.geometry; 125 | const bVerts = geo.attributes.position; 126 | const bColor = geo.attributes.color; //this.geometry.index; 127 | const bConfig = geo.attributes.config; 128 | 129 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 130 | if( this._verts.length > bVerts.array.length || 131 | this._color.length > bColor.array.length || 132 | this._config.length > bConfig.array.length 133 | ){ 134 | if( this.geometry ) this.geometry.dispose(); 135 | this.geometry = _newDynLineMeshGeometry( this._verts, this._color, this._config ); 136 | this._dirty = false; 137 | return; 138 | } 139 | 140 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 141 | bVerts.array.set( this._verts ); 142 | bVerts.count = this._verts.length / 3; 143 | bVerts.needsUpdate = true; 144 | 145 | bColor.array.set( this._color ); 146 | bColor.count = this._color.length / 3; 147 | bColor.needsUpdate = true; 148 | 149 | bConfig.array.set( this._config ); 150 | bConfig.count = this._config.length / 1; 151 | bConfig.needsUpdate = true; 152 | 153 | geo.setDrawRange( 0, bVerts.count ); 154 | geo.computeBoundingBox(); 155 | geo.computeBoundingSphere(); 156 | 157 | this._dirty = false; 158 | } 159 | } 160 | 161 | //#region SUPPORT 162 | function _newDynLineMeshGeometry( aVerts, aColor, aConfig, doCompute=true ){ 163 | //if( !( aVerts instanceof Float32Array) ) aVerts = new Float32Array( aVerts ); 164 | //if( !( aColor instanceof Float32Array) ) aColor = new Float32Array( aColor ); 165 | 166 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 167 | const bVerts = new THREE.Float32BufferAttribute( aVerts, 3 ); 168 | const bColor = new THREE.Float32BufferAttribute( aColor, 3 ); 169 | const bConfig = new THREE.Float32BufferAttribute( aConfig, 1 ); 170 | bVerts.setUsage( THREE.DynamicDrawUsage ); 171 | bColor.setUsage( THREE.DynamicDrawUsage ); 172 | bConfig.setUsage( THREE.DynamicDrawUsage ); 173 | 174 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 175 | const geo = new THREE.BufferGeometry(); 176 | geo.setAttribute( 'position', bVerts ); 177 | geo.setAttribute( 'color', bColor ); 178 | geo.setAttribute( 'config', bConfig ); 179 | 180 | if( doCompute ){ 181 | geo.computeBoundingSphere(); 182 | geo.computeBoundingBox(); 183 | } 184 | return geo; 185 | } 186 | 187 | function glColor( hex, out = null ){ 188 | const NORMALIZE_RGB = 1 / 255; 189 | out = out || [0,0,0]; 190 | 191 | out[0] = ( hex >> 16 & 255 ) * NORMALIZE_RGB; 192 | out[1] = ( hex >> 8 & 255 ) * NORMALIZE_RGB; 193 | out[2] = ( hex & 255 ) * NORMALIZE_RGB; 194 | 195 | return out; 196 | } 197 | //#endregion 198 | 199 | //#region SHADER 200 | 201 | function newDynLineMeshMaterial(){ 202 | return new THREE.RawShaderMaterial({ 203 | depthTest : false, 204 | transparent : true, 205 | uniforms : { 206 | dashSeg : { value : 1 / 0.07 }, 207 | dashDiv : { value : 0.4 }, 208 | }, 209 | vertexShader : `#version 300 es 210 | in vec3 position; 211 | in vec3 color; 212 | in float config; 213 | 214 | uniform mat4 modelViewMatrix; 215 | uniform mat4 projectionMatrix; 216 | uniform float u_scale; 217 | 218 | out vec3 fragColor; 219 | out float fragLen; 220 | 221 | void main(){ 222 | vec4 wPos = modelViewMatrix * vec4( position, 1.0 ); 223 | 224 | fragColor = color; 225 | fragLen = config; 226 | 227 | gl_Position = projectionMatrix * wPos; 228 | }`, 229 | fragmentShader : `#version 300 es 230 | precision mediump float; 231 | 232 | uniform float dashSeg; 233 | uniform float dashDiv; 234 | 235 | in vec3 fragColor; 236 | in float fragLen; 237 | out vec4 outColor; 238 | 239 | void main(){ 240 | float alpha = 1.0; 241 | if( fragLen > 0.0 ) alpha = step( dashDiv, fract( fragLen * dashSeg ) ); 242 | outColor = vec4( fragColor, alpha ); 243 | }`}); 244 | } 245 | 246 | //#endregion 247 | 248 | export default DynLineMesh; -------------------------------------------------------------------------------- /annotatebox/render/ScaleCube.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default class ScaleCube{ 4 | // #region MAIN 5 | mesh = null; 6 | constructor(){ 7 | const geo = genGeometry(); 8 | const bGeo = new THREE.BufferGeometry(); 9 | bGeo.setIndex( new THREE.BufferAttribute( geo.indices, 1 ) ); 10 | bGeo.setAttribute( 'position', new THREE.BufferAttribute( geo.vertices, 4 ) ); 11 | bGeo.setAttribute( 'normal', new THREE.BufferAttribute( geo.normals, 3 ) ); 12 | bGeo.setAttribute( 'uv', new THREE.BufferAttribute( geo.texcoord, 2 ) ); 13 | 14 | const mat = customMaterial(); 15 | this.mesh = new THREE.Mesh( bGeo, mat ); 16 | } 17 | // #endregion 18 | 19 | // #region METHODS 20 | // xp, yp, zp, xn, yn, zn 21 | setAxesLengths( size ){ 22 | const scl = this.mesh.material.uniforms.scale.value; 23 | scl[ 0 ] = size[ 0 ]; 24 | scl[ 1 ] = size[ 1 ]; 25 | scl[ 2 ] = size[ 2 ]; 26 | scl[ 3 ] = size[ 3 ]; 27 | scl[ 4 ] = size[ 4 ]; 28 | scl[ 5 ] = size[ 5 ]; 29 | return this; 30 | } 31 | 32 | selectFace( i ){ this.mesh.material.uniforms.selFace.value = i; return this; } 33 | // #endregion 34 | } 35 | 36 | function genGeometry(){ 37 | const x1 = 1, 38 | y1 = 1, 39 | z1 = 1, 40 | x0 = -1, 41 | y0 = -1, 42 | z0 = -1; 43 | 44 | // Starting bottom left corner, then working counter clockwise to create the front face. 45 | // Backface is the first face but in reverse (3,2,1,0) 46 | // keep each quad face built the same way to make index and uv easier to assign 47 | const vert = [ 48 | x0, y1, z1, 2, //0 Front 49 | x0, y0, z1, 2, //1 50 | x1, y0, z1, 2, //2 51 | x1, y1, z1, 2, //3 52 | 53 | x1, y1, z0, 5, //4 Back 54 | x1, y0, z0, 5, //5 55 | x0, y0, z0, 5, //6 56 | x0, y1, z0, 5, //7 57 | 58 | x1, y1, z1, 0, //3 Right 59 | x1, y0, z1, 0, //2 60 | x1, y0, z0, 0, //5 61 | x1, y1, z0, 0, //4 62 | 63 | x0, y0, z1, 4, //1 Bottom 64 | x0, y0, z0, 4, //6 65 | x1, y0, z0, 4, //5 66 | x1, y0, z1, 4, //2 67 | 68 | x0, y1, z0, 3, //7 Left 69 | x0, y0, z0, 3, //6 70 | x0, y0, z1, 3, //1 71 | x0, y1, z1, 3, //0 72 | 73 | x0, y1, z0, 1, //7 Top 74 | x0, y1, z1, 1, //0 75 | x1, y1, z1, 1, //3 76 | x1, y1, z0, 1, //4 77 | ]; 78 | 79 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | // Build the index of each quad [0,1,2, 2,3,0] 81 | let i; 82 | const idx = []; 83 | for( i=0; i < vert.length / 3; i+=2) idx.push( i, i+1, ( Math.floor( i / 4 ) * 4 ) + ( ( i + 2 ) % 4 ) ); 84 | 85 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 86 | // Build UV data for each vertex 87 | const uv = []; 88 | for( i=0; i < 6; i++) uv.push( 0,0, 0,1, 1,1, 1,0 ); 89 | 90 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 91 | return { 92 | vertices : new Float32Array( vert ), 93 | indices : new Uint16Array( idx ), 94 | texcoord : new Float32Array( uv ), 95 | normals : new Float32Array( [ // Left/Right have their xNormal flipped to render correctly in 3JS, Why does normals need to be mirrored on X? 96 | 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, //Front 97 | 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, //Back 98 | 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, //Left 99 | 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, //Bottom 100 | -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, //Right 101 | 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0 //Top 102 | ] ), 103 | }; 104 | } 105 | 106 | function customMaterial(){ 107 | const mat = new THREE.RawShaderMaterial({ 108 | depthTest : true, 109 | transparent : true, 110 | side : THREE.DoubleSide, 111 | // lights : true, 112 | 113 | uniforms : { 114 | alpha : { type :'float', value:0.7, }, 115 | scale : { type :'float', value:[1,1,1,1,1,1] }, 116 | borderWidth : { type :'float', value:0.02, }, 117 | borderColor : { type :'vec3', value:new THREE.Color( 0xb0b0b0 ) }, 118 | selColor : { type :'vec3', value:new THREE.Color( 0xffff00 ) }, 119 | 120 | selFace : { type :'int', value:-1, }, 121 | 122 | color : { type :'vec3', value:new THREE.Color( 0xa0a0a0 ) }, 123 | color_x : { type :'vec3', value:new THREE.Color( "#878FA3" ) }, // Each axis gets a Grayscaled Value, used as strength of baseColor 124 | color_y : { type :'vec3', value:new THREE.Color( "#ffffff" ) }, // these don't really need to be modified unless looking to change 125 | color_z : { type :'vec3', value:new THREE.Color( "#CED4E0" ) }, // the overall strength of each axis 126 | }, 127 | 128 | extensions : { 129 | derivatives : true 130 | }, 131 | 132 | vertexShader : `#version 300 es 133 | in vec4 position; 134 | in vec3 normal; 135 | in vec2 uv; 136 | 137 | uniform mediump float[6] scale; 138 | uniform int selFace; 139 | uniform vec3 color; 140 | uniform vec3 selColor; 141 | 142 | uniform mat4 modelMatrix; 143 | uniform mat4 viewMatrix; 144 | uniform mat4 projectionMatrix; 145 | 146 | out vec3 fragSPos; 147 | out vec3 fragWPos; // World Space Position 148 | out vec3 fragNorm; 149 | out vec3 fragLNorm; 150 | out vec2 fragUV; 151 | out vec3 fragColor; 152 | 153 | void main(){ 154 | int faceIdx = int( position.w ); 155 | fragColor = (faceIdx != selFace)? color : selColor; 156 | 157 | vec3 pos = position.xyz; 158 | pos.x *= ( pos.x > 0.0 )? scale[ 0 ] : scale[ 3 ]; 159 | pos.y *= ( pos.y > 0.0 )? scale[ 1 ] : scale[ 4 ]; 160 | pos.z *= ( pos.z > 0.0 )? scale[ 2 ] : scale[ 5 ]; 161 | 162 | vec4 wPos = modelMatrix * vec4( pos, 1.0 ); // World Space 163 | vec4 vPos = viewMatrix * wPos; // View Space 164 | 165 | fragSPos = pos; 166 | fragUV = uv; 167 | fragWPos = wPos.xyz; 168 | fragNorm = ( modelMatrix * vec4( normal, 0.0 ) ).xyz; 169 | fragLNorm = normal; 170 | 171 | gl_Position = projectionMatrix * vPos; 172 | }`, 173 | 174 | fragmentShader : `#version 300 es 175 | precision mediump float; 176 | 177 | uniform float[6] scale; 178 | uniform float alpha; 179 | //uniform vec3 color; 180 | uniform vec3 color_x; 181 | uniform vec3 color_y; 182 | uniform vec3 color_z; 183 | uniform float borderWidth; 184 | uniform vec3 borderColor; 185 | 186 | in vec3 fragColor; 187 | in vec3 fragSPos; 188 | in vec3 fragWPos; 189 | in vec3 fragNorm; 190 | in vec3 fragLNorm; 191 | in vec2 fragUV; 192 | out vec4 outColor; 193 | 194 | // ##################################################################### 195 | void main(){ 196 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 197 | vec3 norm = normalize( fragNorm ); // Normals From Mesh 198 | 199 | // Treating normal as Light Strength, it curves the progression from dark to light 200 | // if left as is, it gives the tint lighting much more strength and also linear progression 201 | norm = norm * norm; 202 | 203 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 204 | // From what I understand of how this works is by applying a Lighting Color for Each axis direction. 205 | // Then using the normal direction to blend each axis color together. From kenny's image example, he 206 | // setup the brightest color to come from Y, Second from Z then the darkest color at X. 207 | vec3 out_color; 208 | out_color = mix( fragColor, fragColor * color_x, norm.x ); 209 | out_color = mix( out_color, fragColor * color_y, norm.y ); 210 | out_color = mix( out_color, fragColor * color_z, norm.z ); 211 | 212 | //outColor = vec4( out_color, alpha ); 213 | 214 | // Darken Negative 215 | float aMin = abs( min( fragLNorm.x, min( fragLNorm.y, fragLNorm.z ) ) ); 216 | out_color = out_color - aMin * 0.3 * out_color ; 217 | 218 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 219 | // Create Borders 220 | // Invert AA Normal : [0,1,0] > [1,0,1] 221 | // if norm.z > 0 then xy 222 | // if norm.y > 0 then xz 223 | // if norm.x > 0 then yz 224 | vec3 negate = abs( fragLNorm - 1.0 ); 225 | 226 | // Scaled Length of each of the 6 directions 227 | vec3 scl = fragSPos; 228 | scl.x = ( fragSPos.x > 0.0 )? scale[ 0 ] : scale[ 3 ]; 229 | scl.y = ( fragSPos.y > 0.0 )? scale[ 1 ] : scale[ 4 ]; 230 | scl.z = ( fragSPos.z > 0.0 )? scale[ 2 ] : scale[ 5 ]; 231 | scl -= borderWidth; // Subtract width to simplify smoothstep 232 | 233 | vec3 absPos = abs( fragSPos ); // Absolute Scaled Position 234 | vec3 px = fwidth( absPos ); 235 | float mask = 1.0; 236 | mask = min( mask, smoothstep( scl.x * negate.x, scl.x-px.x, absPos.x ) ); 237 | mask = min( mask, smoothstep( scl.y * negate.y, scl.y-px.y, absPos.y ) ); 238 | mask = min( mask, smoothstep( scl.z * negate.z, scl.z-px.z, absPos.z ) ); 239 | mask = 1.0 - mask; 240 | 241 | outColor.rgb = mix( out_color, borderColor, mask ); 242 | outColor.a = mix( alpha, 1.0, mask ); 243 | }` 244 | }); 245 | 246 | return mat; 247 | } --------------------------------------------------------------------------------