diff --git a/.eslintrc b/.eslintrc
index 4b57ae0..ee6f9f3 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,6 +14,6 @@
"node": true
},
"rules" : {
- "@typescript-eslint/no-inferrable-types" : "on"
+ "@typescript-eslint/no-inferrable-types" : 0
}
}
\ No newline at end of file
diff --git a/prototypes/002_plane_drag_v2.html b/prototypes/002_plane_drag_v2.html
new file mode 100644
index 0000000..c1d51dc
--- /dev/null
+++ b/prototypes/002_plane_drag_v2.html
@@ -0,0 +1,450 @@
+
+
+
\ No newline at end of file
diff --git a/prototypes/002_plane_drag_v2_ts.html b/prototypes/002_plane_drag_v2_ts.html
new file mode 100644
index 0000000..9b21eaf
--- /dev/null
+++ b/prototypes/002_plane_drag_v2_ts.html
@@ -0,0 +1,38 @@
+
+
+
\ No newline at end of file
diff --git a/prototypes/004_polygons.html b/prototypes/004_polygons.html
index 2ba1a7c..77bf99f 100644
--- a/prototypes/004_polygons.html
+++ b/prototypes/004_polygons.html
@@ -37,18 +37,18 @@
// Debug.pnt.add( v.a, 0x00ff00, 2 );
// }
- // const c = Arrows.quad();
- // for( let v of iterFlatVec3Line( c, true ) ){
- // Debug.ln.add( v.a, v.b, 0x00ff00 );
- // Debug.pnt.add( v.a, 0x00ff00, 2 );
- // }
-
- const a = Rect.rounded();
- for( let v of iterFlatVec3Line( a, true ) ){
+ const c = Arrows.quad();
+ for( let v of iterFlatVec3Line( c, true ) ){
Debug.ln.add( v.a, v.b, 0x00ff00 );
Debug.pnt.add( v.a, 0x00ff00, 2 );
}
+ // const a = Rect.rounded();
+ // for( let v of iterFlatVec3Line( a, true ) ){
+ // Debug.ln.add( v.a, v.b, 0x00ff00 );
+ // Debug.pnt.add( v.a, 0x00ff00, 2 );
+ // }
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
App.renderLoop();
});
diff --git a/prototypes/006_shapes.html b/prototypes/006_shapes.html
new file mode 100644
index 0000000..f3a4481
--- /dev/null
+++ b/prototypes/006_shapes.html
@@ -0,0 +1,152 @@
+
+
+
\ No newline at end of file
diff --git a/src/actions/AngleMovementRender.ts b/src/actions/AngleMovementRender.ts
new file mode 100644
index 0000000..47cc3d1
--- /dev/null
+++ b/src/actions/AngleMovementRender.ts
@@ -0,0 +1,52 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+// #region IMPORTS
+import type { PlaneMovement } from './PlaneMovement';
+import ShapePointsMesh from '../render/ShapePointsMesh';
+import DynLineMesh from '../render/DynLineMesh';
+import AngleViewMaterial from '../render/AngleViewMaterial';
+
+import { Group, PlaneGeometry, Mesh }
+ from 'three';
+// #endregion
+
+export default class AngleMovementRender extends Group{
+ // #region MAIN
+ _pnt : any = new ShapePointsMesh();
+ _ln : any = new DynLineMesh();
+ mesh !: Mesh;
+ mat !: any;
+
+ constructor(){
+ super();
+ this.visible = false;
+ this.mat = AngleViewMaterial();
+
+ const geo = new PlaneGeometry( 2, 2 );
+ this.mesh = new Mesh( geo, this.mat );
+ this.add( this.mesh );
+ this.add( this._ln );
+ this.add( this._pnt );
+ }
+ // #endregion
+
+ // #region RENDER INTERFACE
+ render( action: PlaneMovement ){
+ this._pnt.reset().add( action.dragPos, 0xffffff, 5, 2 );
+ this._ln.reset().add( action.origin, action.dragPos, 0xffffff );
+
+ this.mat.radArc = action.dragAngle;
+ }
+
+ postRender(){
+ this.visible = false;
+ }
+
+ preRender( action: PlaneMovement ){
+ this.mesh.position.fromArray( action.origin );
+ this.mesh.quaternion.fromArray( action.rotation );
+ this.mesh.scale.setScalar( action.scale );
+ this.visible = true;
+ }
+ // #endregion
+}
diff --git a/src/actions/LineMovement.ts b/src/actions/LineMovement.ts
index 7ee0128..fba8888 100644
--- a/src/actions/LineMovement.ts
+++ b/src/actions/LineMovement.ts
@@ -6,11 +6,11 @@ import Vec3 from '../maths/Vec3';
// #endregion
export interface ILineMovementHandler{
- onLineInit( ln: LineMovement ): void;
- onLinePosition( pos: ConstVec3, ln: LineMovement ): void;
+ onLineInit( action: LineMovement ): void;
+ onLineUpdate( action: LineMovement, isDone: boolean ): void;
}
-export class LineMovement{
+export class LineMovement implements IAction{
// #region MAIN
steps = 0;
incNeg = true; // Move segment's starting point in the neg direction
@@ -29,7 +29,7 @@ export class LineMovement{
gizmo : ILineMovementHandler | null = null; // Active gizmo requestion this action
- events : EventDispatcher; // Shared Event target to use for dispatching data
+ events : EventDispatcher; // Shared Event target to use for dispatching data, can be used by Gizmos
constructor( et: EventDispatcher ){
this.events = et;
@@ -72,17 +72,21 @@ export class LineMovement{
this.segEnd.copy( end );
return this;
}
+ // #endregion
+ // #region IACTION Implementation
// Set active gizmo
setGizmo( g: ILineMovementHandler ): this{
- this.gizmo = g;
this._reset();
+ this.gizmo = g;
this.gizmo.onLineInit( this );
return this;
}
- // #endregion
- // #region METHODS
+ onUp(): this{
+ this.gizmo?.onLineUpdate( this, true ); return this;
+ }
+
onMove( ray: Ray ): boolean{
if( nearSegment( ray, this.segStart, this.segEnd, this.result ) ){
if( this.steps === 0 ) this.dragPos.fromAdd( this.result.segPosition, this.offset );
@@ -105,7 +109,7 @@ export class LineMovement{
this.dragPos.fromScaleThenAdd( dist, dir, this.anchor );
}
- this.gizmo?.onLinePosition( this.dragPos.slice() as ConstVec3, this );
+ this.gizmo?.onLineUpdate( this, false );
return true;
}
diff --git a/src/actions/PlaneMovement.ts b/src/actions/PlaneMovement.ts
new file mode 100644
index 0000000..11c746e
--- /dev/null
+++ b/src/actions/PlaneMovement.ts
@@ -0,0 +1,110 @@
+// #region IMPORT
+import type Ray from '../ray/Ray';
+import type EventDispatcher from '../util/EventDispatcher';
+import intersectPlane from '../ray/intersectPlane'
+import Vec3 from '../maths/Vec3';
+import Quat from '../maths/Quat';
+// #endregion
+
+export interface IPlaneMovementHandler{
+ onPlaneInit( ln: PlaneMovement ): void;
+ onPlaneUpdate( action: PlaneMovement, isDone: boolean ): void;
+}
+
+export class PlaneMovement implements IAction{
+ // #region MAIN
+ dragPos = new Vec3(); // current position when dragging
+ dragDir = new Vec3(); // Direction to drag point from origin
+ dragAngle = 0; // Radian angle from yAxis
+
+ steps = 0;
+ scale = 1;
+ origin = new Vec3();
+ xAxis = new Vec3( 1, 0, 0 );
+ yAxis = new Vec3( 0, 1, 0 );
+ zAxis = new Vec3( 0, 0, 1 ); // Will be used as normal
+ rotation = new Quat(); // Rotation that represents the AXES
+
+ gizmo : IPlaneMovementHandler | null = null; // Active gizmo requestion this action
+ events : EventDispatcher; // Shared Event target to use for dispatching data, can be used by Gizmos
+
+ constructor( et: EventDispatcher ){
+ this.events = et;
+ }
+ // #endregion
+
+ // #region METHODS
+ _reset(){
+ this.steps = 0;
+ this.scale = 1;
+ }
+
+ setOrigin( v: ConstVec3 ){ this.origin.copy( v ); return this; }
+
+ setQuatDir( q: ConstVec4 ){
+ this.xAxis.fromQuat( q, [1,0,0] );
+ this.yAxis.fromQuat( q, [0,1,0] );
+ this.zAxis.fromQuat( q, [0,0,1] );
+ this.rotation.copy( q );
+ return this;
+ }
+
+ setAxes( x: ConstVec4, y: ConstVec4, z: ConstVec4 ){
+ this.xAxis.copy( x );
+ this.yAxis.copy( y );
+ this.zAxis.copy( z );
+ this.rotation.fromAxes( x, y, z );
+ return this;
+ }
+
+ setScale( s: number ){ this.scale = s; return this; }
+ // #endregion
+
+ // #region IACTION Implementation
+ // Set active gizmo
+ setGizmo( g:IPlaneMovementHandler ){
+ this._reset();
+ this.gizmo = g;
+ this.gizmo.onPlaneInit( this );
+ return this;
+ }
+ // #endregion
+
+ // #region METHODS
+ onUp(){ this.gizmo?.onPlaneUpdate( this, true ); }
+
+ onMove( ray: Ray ){
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const t = intersectPlane( ray, this.origin, this.zAxis );
+ if( t == null ) return false;
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Get the intersect position
+ if( this.steps === 0 ) ray.posAt( t, this.dragPos );
+ else{
+ // Step the intersect position
+ ray.posAt( t, this.dragPos );
+
+ this.dragPos.sub( this.origin );
+
+ const xDist = Math.round( Vec3.projectScale( this.dragPos, this.xAxis ) / this.steps ) * this.steps;
+ const yDist = Math.round( Vec3.projectScale( this.dragPos, this.yAxis ) / this.steps ) * this.steps;
+
+ this.dragPos
+ .copy( this.origin )
+ .scaleThenAdd( xDist, this.xAxis )
+ .scaleThenAdd( yDist, this.yAxis );
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ this.dragDir.fromSub( this.dragPos, this.origin ).norm();
+ this.dragAngle = Vec3.angle( this.yAxis, this.dragDir );
+
+ if( Vec3.dot( this.dragDir, this.xAxis ) > 0 ) this.dragAngle = -this.dragAngle;
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ this.gizmo?.onPlaneUpdate( this, false );
+ return true;
+ }
+ // #endregion
+}
\ No newline at end of file
diff --git a/src/geo/Tear.ts b/src/geo/Tear.ts
new file mode 100644
index 0000000..08bce5f
--- /dev/null
+++ b/src/geo/Tear.ts
@@ -0,0 +1,60 @@
+export default function tearShape( radius=1, steps=24, power=8, pull=0.4 ){
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Generate half shape
+ const hStep : number = steps / 2;
+ const inc : number = (Math.PI * 2.0) / steps;
+ const arc : Array = [];
+ let v : TVec3 = [0,0,0];
+ let rad : number;
+ let r : number;
+ let i : number;
+
+ for( i=0; i <= hStep; i++ ){
+ rad = inc * i + Math.PI * 0.5;
+ r = ( i <= hStep )
+ ? (1-( i/hStep )) ** power * pull + radius
+ : radius;
+
+ planeCircle( [0,0,0], [1,0,0], [0,1,0], rad, r, v );
+ arc.push( v.slice() );
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Generate mesh vertices from the half shape
+
+ const verts: Array = [];
+ // Front Face
+ for( v of arc ){ verts.push( v[0], v[1], 0.1 ); }
+ for( i=arc.length-2; i > 0; i-- ){ v = arc[ i ]; verts.push( -v[0], v[1], 0.1 ); }
+
+ // Back Face
+ for( v of arc ){ verts.push( v[0], v[1], -0.1 ); }
+ for( i=arc.length-2; i > 0; i-- ){ v = arc[ i ]; verts.push( -v[0], v[1], -0.1 ); }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const indices : Array = [];
+ let ii : number;
+ let b : number;
+ let c : number;
+ for( let i=0; i < steps; i++ ){
+ ii = i + steps;
+ c = (i + 1) % steps;
+ b = ((i + 1) % steps) + steps;
+ indices.push( i, ii, b, b, c, i );
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ return {
+ vertices : new Float32Array( verts ),
+ indices : new Uint16Array( indices ),
+ };
+}
+
+function planeCircle( center: ConstVec3, xAxis: ConstVec3, yAxis: ConstVec3, angle: number, radius: number, out: TVec3 ): TVec3{
+ const sin = Math.sin( angle );
+ const cos = Math.cos( angle );
+ out[0] = center[0] + radius * cos * xAxis[0] + radius * sin * yAxis[0];
+ out[1] = center[1] + radius * cos * xAxis[1] + radius * sin * yAxis[1];
+ out[2] = center[2] + radius * cos * xAxis[2] + radius * sin * yAxis[2];
+ return out;
+}
\ No newline at end of file
diff --git a/src/gizmos.ts b/src/gizmos.ts
index 71a9b08..5b091ef 100644
--- a/src/gizmos.ts
+++ b/src/gizmos.ts
@@ -1,4 +1,5 @@
// #region IMPORTS
+/* eslint-disable @typescript-eslint/no-explicit-any */
import type { WebGLRenderer, Camera, Scene, Object3D } from 'three';
import Ray from './ray/Ray';
@@ -7,6 +8,9 @@ import MouseHandlers from './util/MouseHandlers';
import { LineMovement } from './actions/LineMovement';
import LineMovementRender from './actions/LineMovementRender';
+
+import { PlaneMovement } from './actions/PlaneMovement';
+import AngleMovementRender from './actions/AngleMovementRender';
// #endregion
// Gizmos are 3D Objects that must have implemented gizmo interface
@@ -21,14 +25,15 @@ export default class Gizmos{
scene : Scene; // Scene to add gizmos + support
camera : Camera; // Scene's camera
- list : Array< TGizmo3D > = new Array(); // List of available gizmos
+ list : Array< TGizmo3D > = []; // List of available gizmos
dragGizmo : TGizmo3D | null = null; // Currently active gizmo
dragAction : any = null; // Currently used action
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
actions : { [key:string]:any } = {
- line : { handler: new LineMovement( this.events ), renderer: new LineMovementRender() },
- // plane : { handler: null, renderer: null },
+ line : { handler: new LineMovement( this.events ), renderer: new LineMovementRender() },
+ plane : { handler: new PlaneMovement( this.events ), renderer: new AngleMovementRender() },
};
constructor( renderer: WebGLRenderer, camera: Camera, scene: Scene ){
@@ -38,6 +43,7 @@ export default class Gizmos{
this.mouse = new MouseHandlers( this.canvas, { down: this.onDown, move: this.onMove, up: this.onUp } );
scene.add( this.actions.line.renderer );
+ scene.add( this.actions.plane.renderer );
}
// #endregion
@@ -50,7 +56,7 @@ export default class Gizmos{
updateCameraScale(){
const pos = this.camera.position.toArray();
- for( let g of this.list ){
+ for( const g of this.list ){
if( g.visible ) g.onCameraScale( pos );
}
}
@@ -70,7 +76,7 @@ export default class Gizmos{
this._updateRay( pos );
let action : string | null = null;
- for( let g of this.list ){
+ for( const g of this.list ){
if( g.visible ){
// Check if this gizmo is a hit & which action it needs to use
@@ -96,14 +102,15 @@ export default class Gizmos{
return false;
};
- onUp = ( _e: PointerEvent, _pos: ConstVec2 ):void =>{
+ onUp = ():void =>{ //_e: PointerEvent, _pos: ConstVec2
if( this.dragGizmo ){
- this.dragGizmo.onUp(); // Complete drag event
- this.dragGizmo = null; // No longer active for action
-
+ this.dragAction.handler.onUp(); // Tell action dragging is complete
this.dragAction.renderer.postRender(); // Cleanup any rendering
this.dragAction = null; // No action active
-
+
+ this.dragGizmo.onUp(); // Complete drag event
+ this.dragGizmo = null; // No longer active for action
+
this.events.emit( 'dragStop' ); // Alert parent that dragging is over
}
};
@@ -117,7 +124,7 @@ export default class Gizmos{
this.dragAction.renderer.render( this.dragAction.handler );
}else{
// No active action, pass ray to any gizmo for onHover visualization
- for( let g of this.list ){
+ for( const g of this.list ){
if( g.visible ) g.onHover( this.ray );
}
}
diff --git a/src/gizmos/TranslateGizmo.ts b/src/gizmos/TranslateGizmo.ts
index 8d727ac..0998076 100644
--- a/src/gizmos/TranslateGizmo.ts
+++ b/src/gizmos/TranslateGizmo.ts
@@ -106,21 +106,22 @@ export default class Translation extends Group implements IGizmo, ILineMovementH
// #endregion
// #region LINE ACTION HANDLERS
- onLineInit( ln: LineMovement ){
- ln.steps = 0;
- ln.incNeg = true;
+ onLineInit( action: LineMovement ){
+ action.steps = 0;
+ action.incNeg = true;
const tmp = new Vec3( this.state.position ).sub( this._hitPos );
- ln.setOffset( tmp );
+ action.setOffset( tmp );
- ln.setDirection( this._axes[ this._selAxis ] );
- ln.setOrigin( this.state.position );
- ln.recompute();
+ action.setDirection( this._axes[ this._selAxis ] );
+ action.setOrigin( this.state.position );
+ action.recompute();
}
- onLinePosition( pos: ConstVec3, ln: LineMovement ){
+ onLineUpdate( action: LineMovement, isDone: boolean ){
+ const pos = action.dragPos.slice();
this.state.position = pos;
- ln.events.emit( 'translate', { position:pos, gizmo:this } );
+ action.events.emit( 'translate', { position:pos, gizmo:this, isDone } );
}
// #endregion
diff --git a/src/gizmos/TwistGizmo.ts b/src/gizmos/TwistGizmo.ts
new file mode 100644
index 0000000..08d162a
--- /dev/null
+++ b/src/gizmos/TwistGizmo.ts
@@ -0,0 +1,139 @@
+// #region IMPORTS
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type Ray from '../ray/Ray';
+import type {
+ PlaneMovement, IPlaneMovementHandler
+} from '../actions/PlaneMovement';
+
+import { intersectSphere } from '../ray/intersectSphere';
+import StateProxy from '../util/StateProxy';
+import Vec3 from '../maths/Vec3';
+import Quat from '../maths/Quat';
+
+import Util3JS from '../render/Util3JS';
+import tearShape from '../geo/Tear';
+
+import {
+ Group,
+ MeshBasicMaterial,
+ Mesh,
+ DoubleSide,
+ } from 'three';
+// #endregion
+
+export default class TwistGizmo extends Group implements IGizmo, IPlaneMovementHandler{
+ // #region MAIN
+ _shape !: Mesh;
+ _mat : any;
+ _xDir = new Vec3( [1,0,0] ); // Generate Axes
+ _yDir = new Vec3( [0,1,0] );
+ _zDir = new Vec3( [0,0,1] );
+ _isOver = false;
+
+ state = StateProxy.new({
+ rotation : [0,0,0,1],
+ center : [0,0,0], // Final position
+ scale : 1, // How to scale the gizmo & action
+ });
+
+ constructor(){
+ super();
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const proxy = this.state.$;
+ proxy.on( 'change', this.onStateChange );
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const geo = Util3JS.geoBuffer( tearShape() );
+ geo.computeVertexNormals();
+
+ this._mat = new MeshBasicMaterial( { side : DoubleSide, color:0xffffff } );
+ this._shape = new Mesh( geo, this._mat );
+ this.add( this._shape );
+ }
+
+ onStateChange = ( e: CustomEvent )=>{
+ switch( e.detail.prop ){
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ case 'rotation' :{
+ this._xDir.fromQuat( this.state.rotation, [1,0,0] );
+ this._yDir.fromQuat( this.state.rotation, [0,1,0] );
+ this._zDir.fromQuat( this.state.rotation, [0,0,1] );
+ this._render();
+ break;
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ case 'center' : this.position.fromArray( this.state.center ); break;
+ case 'scale' : this.scale.setScalar( this.state.scale ); break;
+ }
+ };
+ // #endregion
+
+ // #region GIZMO INTERFACE
+ // Handle Over event, change visual look when mouse is over gizmo
+ onHover( ray: Ray ){
+ const hit = this._isHit( ray );
+
+ if( this._isOver !== hit ){
+ this._isOver = hit;
+ this._render();
+ }
+
+ return hit;
+ }
+
+ // Which action to perform on mouse down?
+ onDown( ray: Ray ){
+ const hit = ( this._isHit( ray ) );
+
+ if( hit ) this.visible = false;
+
+ return ( hit )? 'plane' : null;
+ }
+
+ // Handle action completion
+ onUp(){
+ this._isOver = false;
+ this._render();
+ this.visible = true;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onCameraScale(): void{} // _camPos: ConstVec3
+ // #endregion
+
+ // #region PLANE ACTION INTERFACE
+ // set initial values for action
+ onPlaneInit( action: PlaneMovement ){
+ action
+ .setOrigin( this.state.center )
+ .setQuatDir( this.state.rotation )
+ .setScale( this.state.scale );
+ }
+
+ // get action results on drag
+ onPlaneUpdate( action: PlaneMovement, isDone: boolean ){
+ const q = new Quat()
+ .fromAxisAngle( action.zAxis, action.dragAngle )
+ .mul( action.rotation );
+
+ if( isDone ) this.state.rotation = q;
+
+ action.events.emit( 'twist', { rotation:q, gizmo:this, isDone } );
+ }
+ // #endregion
+
+ // #region SUPPORT
+ _render(){
+ const color = this._isOver? 0xffffff : 0x999999;
+ this._mat.color.set( color );
+
+ this.quaternion.fromArray( this.state.rotation );
+ }
+
+ _isHit( ray: Ray ){
+ return intersectSphere( ray, this.state.center, 1 );
+ }
+ // #endregion
+}
\ No newline at end of file
diff --git a/src/global.d.ts b/src/global.d.ts
index e040c7e..314d14e 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -26,4 +26,11 @@ declare global{
onDown( ray: Ray ) : string | null;
onUp() : void;
}
+
+ interface IAction{
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ setGizmo( g: any ): this;
+ onUp(): void;
+ onMove( ray: Ray ): boolean;
+ }
}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 88660e4..f514743 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,9 @@
import Gizmos from './Gizmos';
import TranslateGizmo from './gizmos/TranslateGizmo';
+import TwistGizmo from './gizmos/TwistGizmo';
export {
Gizmos,
TranslateGizmo,
+ TwistGizmo,
};
\ No newline at end of file
diff --git a/src/maths/Quat.ts b/src/maths/Quat.ts
new file mode 100644
index 0000000..55a2113
--- /dev/null
+++ b/src/maths/Quat.ts
@@ -0,0 +1,123 @@
+
+export default class Quat extends Array< number >{
+ // #region MAIN
+ constructor( v ?: ConstVec4 ){
+ super( 4 );
+
+ if( v instanceof Quat || v instanceof Float32Array || ( v instanceof Array && v.length == 4 ) ){
+ this[ 0 ] = v[ 0 ];
+ this[ 1 ] = v[ 1 ];
+ this[ 2 ] = v[ 2 ];
+ this[ 3 ] = v[ 3 ];
+ }else{
+ this[ 0 ] = 0;
+ this[ 1 ] = 0;
+ this[ 2 ] = 0;
+ this[ 3 ] = 1;
+ }
+ }
+ // #endregion
+
+ // #region SETTERS / GETTERS
+ copy( a: ConstVec4 ): this{
+ this[ 0 ] = a[ 0 ];
+ this[ 1 ] = a[ 1 ];
+ this[ 2 ] = a[ 2 ];
+ this[ 3 ] = a[ 3 ];
+ return this
+ }
+ // #endregion
+
+ // #region FROM SETTERS
+ /** Axis must be normlized, Angle in Radians */
+ fromAxisAngle( axis: ConstVec3, rad: number ): this{
+ const half = rad * 0.5;
+ const s = Math.sin( half );
+ this[ 0 ] = axis[ 0 ] * s;
+ this[ 1 ] = axis[ 1 ] * s;
+ this[ 2 ] = axis[ 2 ] * s;
+ this[ 3 ] = Math.cos( half );
+ return this;
+ }
+
+ fromAxes( xAxis: ConstVec3, yAxis: ConstVec3, zAxis: ConstVec3 ): this{
+ const m00 = xAxis[0], m01 = xAxis[1], m02 = xAxis[2],
+ m10 = yAxis[0], m11 = yAxis[1], m12 = yAxis[2],
+ m20 = zAxis[0], m21 = zAxis[1], m22 = zAxis[2],
+ t = m00 + m11 + m22;
+ let x, y, z, w, s;
+
+ if(t > 0.0){
+ s = Math.sqrt(t + 1.0);
+ w = s * 0.5 ; // |w| >= 0.5
+ s = 0.5 / s;
+ x = (m12 - m21) * s;
+ y = (m20 - m02) * s;
+ z = (m01 - m10) * s;
+ }else if((m00 >= m11) && (m00 >= m22)){
+ s = Math.sqrt(1.0 + m00 - m11 - m22);
+ x = 0.5 * s;// |x| >= 0.5
+ s = 0.5 / s;
+ y = (m01 + m10) * s;
+ z = (m02 + m20) * s;
+ w = (m12 - m21) * s;
+ }else if(m11 > m22){
+ s = Math.sqrt(1.0 + m11 - m00 - m22);
+ y = 0.5 * s; // |y| >= 0.5
+ s = 0.5 / s;
+ x = (m10 + m01) * s;
+ z = (m21 + m12) * s;
+ w = (m20 - m02) * s;
+ }else{
+ s = Math.sqrt(1.0 + m22 - m00 - m11);
+ z = 0.5 * s; // |z| >= 0.5
+ s = 0.5 / s;
+ x = (m20 + m02) * s;
+ y = (m21 + m12) * s;
+ w = (m01 - m10) * s;
+ }
+
+ this[ 0 ] = x;
+ this[ 1 ] = y;
+ this[ 2 ] = z;
+ this[ 3 ] = w;
+ return this;
+ }
+ // #endregion
+
+ // #region OPERATORS
+ /** Multiple Quaternion onto this Quaternion */
+ mul( q: ConstVec4 ): Quat{
+ const ax = this[0], ay = this[1], az = this[2], aw = this[3],
+ bx = q[0], by = q[1], bz = q[2], bw = q[3];
+ this[ 0 ] = ax * bw + aw * bx + ay * bz - az * by;
+ this[ 1 ] = ay * bw + aw * by + az * bx - ax * bz;
+ this[ 2 ] = az * bw + aw * bz + ax * by - ay * bx;
+ this[ 3 ] = aw * bw - ax * bx - ay * by - az * bz;
+ return this;
+ }
+
+ /** PreMultiple Quaternions onto this Quaternion */
+ pmul( q: ConstVec4 ): Quat{
+ const ax = q[0], ay = q[1], az = q[2], aw = q[3],
+ bx = this[0], by = this[1], bz = this[2], bw = this[3];
+ this[ 0 ] = ax * bw + aw * bx + ay * bz - az * by;
+ this[ 1 ] = ay * bw + aw * by + az * bx - ax * bz;
+ this[ 2 ] = az * bw + aw * bz + ax * by - ay * bx;
+ this[ 3 ] = aw * bw - ax * bx - ay * by - az * bz;
+ return this;
+ }
+
+ norm(): this{
+ let len = this[0]**2 + this[1]**2 + this[2]**2 + this[3]**2;
+ if( len > 0 ){
+ len = 1 / Math.sqrt( len );
+ this[ 0 ] *= len;
+ this[ 1 ] *= len;
+ this[ 2 ] *= len;
+ this[ 3 ] *= len;
+ }
+ return this;
+ }
+ // #endregion
+}
\ No newline at end of file
diff --git a/src/maths/Vec3.ts b/src/maths/Vec3.ts
index 478fc92..96f19d5 100644
--- a/src/maths/Vec3.ts
+++ b/src/maths/Vec3.ts
@@ -97,6 +97,16 @@ export default class Vec3 extends Array< number >{
this[ 2 ] = vz + 2 * z2;
return this;
}
+
+ fromCross( a: ConstVec3, b: ConstVec3 ): this{
+ const ax = a[0], ay = a[1], az = a[2],
+ bx = b[0], by = b[1], bz = b[2];
+
+ this[ 0 ] = ay * bz - az * by;
+ this[ 1 ] = az * bx - ax * bz;
+ this[ 2 ] = ax * by - ay * bx;
+ return this;
+ }
// #endregion
// #region OPERATORS
@@ -131,6 +141,14 @@ export default class Vec3 extends Array< number >{
}
return this;
}
+
+
+ scaleThenAdd( scale: number, a: ConstVec3 ): this{
+ this[0] += a[0] * scale;
+ this[1] += a[1] * scale;
+ this[2] += a[2] * scale;
+ return this;
+ }
// #endregion
// #region STATIC
@@ -141,5 +159,37 @@ export default class Vec3 extends Array< number >{
static distSqr( a: ConstVec3, b: ConstVec3 ): number{ return (a[ 0 ]-b[ 0 ]) ** 2 + (a[ 1 ]-b[ 1 ]) ** 2 + (a[ 2 ]-b[ 2 ]) ** 2; }
static dot( a: ConstVec3, b: ConstVec3 ): number { return a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] + a[ 2 ] * b[ 2 ]; }
+
+ // Scale SRC in relation to TARGET
+ static projectScale( from: ConstVec3, to: ConstVec3 ) : number{
+ // Modified project from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Vector3.cs#L265
+ // dot( a, b ) / dot( b, b ) * b
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const denom = this.dot( to, to );
+ return ( denom < 0.000001 )? 0 : this.dot( from, to ) / denom;
+ }
+
+ static angle( a: ConstVec3, b: ConstVec3 ): number{
+ //acos(dot(a,b)/(len(a)*len(b)))
+ //let theta = this.dot( a, b ) / ( Math.sqrt( a.lenSqr * b.lenSqr ) );
+ //return Math.acos( Math.max( -1, Math.min( 1, theta ) ) ); // clamp ( t, -1, 1 )
+
+ // atan2(len(cross(a,b)),dot(a,b))
+ const d = this.dot( a, b ),
+ c = new Vec3().fromCross( a, b );
+ return Math.atan2( Vec3.len(c), d );
+
+ // This also works, but requires more LEN / SQRT Calls
+ // 2 * atan2( ( u * v.len - v * u.len ).len, ( u * v.len + v * u.len ).len );
+
+ //https://math.stackexchange.com/questions/1143354/numerically-stable-method-for-angle-between-3d-vectors/1782769
+ // θ=2 atan2(|| ||v||u−||u||v ||, || ||v||u+||u||v ||)
+
+ //let cosine = this.dot( a, b );
+ //if(cosine > 1.0) return 0;
+ //else if(cosine < -1.0) return Math.PI;
+ //else return Math.acos( cosine / ( Math.sqrt( a.lenSqr * b.lenSqr() ) ) );
+ }
+
// #endregion
}
\ No newline at end of file
diff --git a/src/ray/intersectPlane.ts b/src/ray/intersectPlane.ts
index 19ff16a..d900f0c 100644
--- a/src/ray/intersectPlane.ts
+++ b/src/ray/intersectPlane.ts
@@ -1,14 +1,20 @@
import type Ray from './Ray';
-import { vec3 } from 'gl-matrix';
+import Vec3 from '../maths/Vec3';
/** T returned is scale to vector length, not direction */
-export default function intersectPlane( ray:Ray, planePos: vec3, planeNorm: vec3 ) : number | null {
+export default function intersectPlane( ray:Ray, planePos: ConstVec3, planeNorm: ConstVec3 ) : number | null {
// ((planePos - rayOrigin) dot planeNorm) / ( rayVecLen dot planeNorm )
// pos = t * rayVecLen + rayOrigin;
- const denom = vec3.dot( ray.vecLength, planeNorm ); // Dot product of ray Length and plane normal
+ const denom = Vec3.dot( ray.vecLength, planeNorm ); // Dot product of ray Length and plane normal
if( denom <= 0.000001 && denom >= -0.000001 ) return null; // abs(denom) < epsilon, using && instead to not perform absolute.
- const t = vec3.dot( vec3.sub( [0,0,0], planePos, ray.posStart ), planeNorm ) / denom;
+ const v: TVec3 = [
+ planePos[0] - ray.posStart[0],
+ planePos[1] - ray.posStart[1],
+ planePos[2] - ray.posStart[2],
+ ];
+
+ const t = Vec3.dot( v, planeNorm ) / denom;
return ( t >= 0 )? t : null;
}
diff --git a/src/ray/intersectSphere.ts b/src/ray/intersectSphere.ts
new file mode 100644
index 0000000..9b0f5cc
--- /dev/null
+++ b/src/ray/intersectSphere.ts
@@ -0,0 +1,61 @@
+import Vec3 from '../maths/Vec3';
+import Ray from './Ray';
+
+export class RaySphereResult{
+ tMin = 0; // 0 > 1
+ tMax = 0; // 0 > 1
+ posEntry = [0,0,0];
+ posExit = [0,0,0];
+}
+
+// This function is the better Sphere intersection BUT its for an infinite ray
+// So the T value is creates is for the Ray.Dir instead of Ray.vec_len
+export function intersectSphere( ray: Ray, origin: ConstVec3, radius: number, results ?: RaySphereResult ): boolean{
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ const radiusSq = radius * radius;
+ const rayToCenter = new Vec3( origin ).sub( ray.posStart );
+ const tProj = Vec3.dot( rayToCenter, ray.direction ); // Project the length to the center onto the Ray
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Get length of projection point to center and check if its within the sphere
+ // Opposite^2 = hyptenuse^2 - adjacent^2
+ const oppLenSq = Vec3.lenSqr( rayToCenter ) - ( tProj * tProj );
+ if( oppLenSq > radiusSq ) return false;
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ if( results ){
+ // -----------------------------
+ // if a parallel ray right on the radius, exit & entry is the same
+ if( oppLenSq == radiusSq ){
+ results.tMin = tProj;
+ results.tMax = tProj;
+
+ ray.directionAt( tProj, results.posEntry );
+
+ results.posExit[ 0 ] = results.posEntry[ 0 ];
+ results.posExit[ 1 ] = results.posEntry[ 1 ];
+ results.posExit[ 2 ] = results.posEntry[ 2 ];
+ return true;
+ }
+
+ // -----------------------------
+ // Separate positions for entry and exit
+ const oLen = Math.sqrt( radiusSq - oppLenSq ); // Opposite = sqrt( hyptenuse^2 - adjacent^2 )
+ const t0 = tProj - oLen;
+ const t1 = tProj + oLen;
+
+ // Swap
+ if( t1 < t0 ){
+ results.tMin = t1;
+ results.tMax = t0;
+ }else{
+ results.tMin = t0;
+ results.tMax = t1;
+ }
+
+ ray.directionAt( t0, results.posEntry );
+ ray.directionAt( t1, results.posExit );
+ }
+
+ return true;
+}
\ No newline at end of file
diff --git a/src/render/AngleViewMaterial.ts b/src/render/AngleViewMaterial.ts
new file mode 100644
index 0000000..acf7fff
--- /dev/null
+++ b/src/render/AngleViewMaterial.ts
@@ -0,0 +1,130 @@
+// @ts-nocheck
+
+import * as THREE from 'three';
+
+export default function AngleViewMaterials(): THREE.Material{
+ const mat = new THREE.RawShaderMaterial({
+ depthTest : true,
+ side : THREE.DoubleSide,
+ transparent : true,
+
+ uniforms : {
+ radArc : { type :'float', value: 45 * Math.PI / 180 },
+ radAngle : { type :'float', value: 0 },
+ },
+
+ extensions : {
+ derivatives : true
+ },
+
+ vertexShader : `#version 300 es
+ in vec3 position;
+ in vec3 normal;
+ in vec2 uv;
+
+ uniform mat4 modelMatrix;
+ uniform mat4 viewMatrix;
+ uniform mat4 projectionMatrix;
+
+ out vec3 fragWPos; // World Space Position
+ out vec3 fragNorm;
+ out vec2 fragUV;
+
+ // ################################################################
+
+ void main(){
+ vec4 wPos = modelMatrix * vec4( position, 1.0 ); // World Space
+ vec4 vPos = viewMatrix * wPos; // View Space
+
+ fragUV = uv;
+ fragWPos = wPos.xyz;
+ fragNorm = ( modelMatrix * vec4( normal, 0.0 ) ).xyz;
+
+ gl_Position = projectionMatrix * vPos;
+ }`,
+
+ fragmentShader : `#version 300 es
+ precision mediump float;
+
+ in vec3 fragWPos;
+ in vec3 fragNorm;
+ in vec2 fragUV;
+ out vec4 outColor;
+
+ uniform float radArc;
+ uniform float radAngle;
+
+ // ################################################################
+
+ float ring( vec2 coord, float outer, float inner ){
+ float radius = dot( coord, coord );
+ float dxdy = fwidth( radius );
+ return smoothstep( inner - dxdy, inner + dxdy, radius ) -
+ smoothstep( outer - dxdy, outer + dxdy, radius );
+ }
+
+ float circle( vec2 coord, float outer ){
+ float radius = dot( coord, coord );
+ float dxdy = fwidth( radius );
+ return 1.0 - smoothstep( outer - dxdy, outer + dxdy, radius );
+ }
+
+ // https://www.shadertoy.com/view/XtXyDn
+ float arc( vec2 uv, vec2 up, float angle, float radius, float thick ){
+ float hAngle = angle * 0.5;
+
+ // vector from the circle origin to the middle of the arc
+ float c = cos( hAngle );
+
+ // smoothing perpendicular to the arc
+ float d1 = abs( length( uv ) - radius ) - thick;
+ float w1 = 1.5 * fwidth( d1 ); // proportional to how much d1 change between pixels
+ float s1 = smoothstep( w1 * 0.5, -w1 * 0.5, d1 );
+
+ // smoothing along the arc
+ float d2 = dot( up, normalize( uv ) ) - c;
+ float w2 = 1.5 * fwidth( d2 ); // proportional to how much d2 changes between pixels
+ float s2 = smoothstep( w2 * 0.5, -w2 * 0.5, d2 );
+
+ // mix perpendicular and parallel smoothing
+ return s1 * ( 1.0 - s2 );
+ }
+
+ // ################################################################
+
+ void main(){
+ vec2 uv = fragUV * 2.0 - 1.0; // Remap 0:1 to -1:1
+ // vec3 norm = normalize( fragNorm );
+ // outColor = vec4( norm, 1.0 );
+
+ float mask = 0.0;
+ float radOffset = radians( 90.0 );
+ // float radAngle = radians( 0.0 ) + radOffset;
+ // float radArc = radians( 90.0 );
+
+ float radDir = radAngle + radOffset + radArc * 0.5;
+ vec2 centerDir = vec2( cos( radDir ), sin( radDir ) );
+
+ mask = arc( uv, centerDir, radArc, 0.60, 0.25 );
+ mask = max( mask, ring( uv, 0.98, 0.85 ) );
+ mask = max( mask, circle( uv, 0.08 ) );
+
+ outColor.rgb = vec3( mask );
+ outColor.a = mask;
+ }`
+ });
+
+ Object.defineProperty( mat, 'degAngle', {
+ set: ( v )=>{ mat.uniforms.radAngle.value = v * Math.PI / 180; }
+ });
+
+ Object.defineProperty( mat, 'degArc', {
+ set: ( v )=>{ mat.uniforms.radArc.value = v * Math.PI / 180; }
+ });
+
+ Object.defineProperty( mat, 'radArc', {
+ set: ( v )=>{ mat.uniforms.radArc.value = v; }
+ });
+
+ return mat;
+}
\ No newline at end of file
diff --git a/src/render/Util3JS.ts b/src/render/Util3JS.ts
new file mode 100644
index 0000000..5fa151d
--- /dev/null
+++ b/src/render/Util3JS.ts
@@ -0,0 +1,18 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { BufferGeometry, BufferAttribute } from 'three';
+
+export default class Util3JS{
+
+ static geoBuffer( props: any ): BufferGeometry{
+ const geo = new BufferGeometry();
+ geo.setAttribute( 'position', new BufferAttribute( props.vertices, 3 ) );
+
+ if( props.indices ) geo.setIndex( new BufferAttribute( props.indices, 1 ) );
+ if( props.normal ) geo.setAttribute( 'normal', new BufferAttribute( props.normal, 3 ) );
+ if( props.uv ) geo.setAttribute( 'uv', new BufferAttribute( props.uv, 2 ) );
+
+ return geo;
+ }
+
+}
+
diff --git a/vite.config.js b/vite.config.js
index 2bf08e2..3d0d648 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,6 @@
-import packageJson from "./package.json";
-import path from "path";
-import { defineConfig } from "vite";
-//import { directoryPlugin } from 'vite-plugin-list-directory-contents/dist/plugin.js';
+import packageJson from './package.json';
+import path from 'path';
+import { defineConfig } from 'vite';
import { directoryPlugin } from 'vite-plugin-list-directory-contents';
const fileName = {
@@ -32,14 +31,14 @@ export default defineConfig(({ command, mode, ssrBuild }) => {
} else {
// command === 'build'
return {
- base : "./",
+ base : './',
build : {
minify : false,
- target : "esnext",
+ target : 'esnext',
lib : {
- entry : path.resolve(__dirname, "src/gizmos.ts"),
+ entry : path.resolve( __dirname, 'src/gizmos.ts' ),
name : packageJson.name,
- formats : ["es", "cjs", "iife"],
+ formats : [ 'es', 'cjs', 'iife' ],
fileName : ( format )=>fileName[format],
},
},