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], }, },