copper3d 3.4.9 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Renderer/baseRenderer.d.ts +3 -0
- package/dist/Renderer/baseRenderer.js +20 -0
- package/dist/Renderer/baseRenderer.js.map +1 -1
- package/dist/Renderer/copperMSceneRenderer.d.ts +3 -0
- package/dist/Renderer/copperMSceneRenderer.js +22 -0
- package/dist/Renderer/copperMSceneRenderer.js.map +1 -1
- package/dist/Renderer/copperRenderer.d.ts +0 -2
- package/dist/Renderer/copperRenderer.js +0 -5
- package/dist/Renderer/copperRenderer.js.map +1 -1
- package/dist/Scene/copperScene.d.ts +12 -0
- package/dist/Scene/copperScene.js +38 -44
- package/dist/Scene/copperScene.js.map +1 -1
- package/dist/Utils/segmentation/DrawToolCore.d.ts +3 -0
- package/dist/Utils/segmentation/DrawToolCore.js +29 -1
- package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +54 -0
- package/dist/Utils/segmentation/NrrdTools.js +119 -0
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/core/index.d.ts +1 -1
- package/dist/Utils/segmentation/core/index.js +1 -1
- package/dist/Utils/segmentation/core/index.js.map +1 -1
- package/dist/Utils/segmentation/core/types.d.ts +11 -1
- package/dist/Utils/segmentation/core/types.js +30 -0
- package/dist/Utils/segmentation/core/types.js.map +1 -1
- package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -0
- package/dist/Utils/segmentation/eventRouter/EventRouter.js +37 -6
- package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
- package/dist/Utils/segmentation/eventRouter/types.d.ts +3 -2
- package/dist/Utils/segmentation/tools/AiAssistTool.d.ts +146 -0
- package/dist/Utils/segmentation/tools/AiAssistTool.js +470 -0
- package/dist/Utils/segmentation/tools/AiAssistTool.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/MeshGraph.d.ts +40 -0
- package/dist/Utils/surfaceAnnotation/MeshGraph.js +198 -0
- package/dist/Utils/surfaceAnnotation/MeshGraph.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.d.ts +103 -0
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.js +402 -0
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/annotationStore.d.ts +37 -0
- package/dist/Utils/surfaceAnnotation/annotationStore.js +105 -0
- package/dist/Utils/surfaceAnnotation/annotationStore.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/contourRender.d.ts +7 -0
- package/dist/Utils/surfaceAnnotation/contourRender.js +57 -0
- package/dist/Utils/surfaceAnnotation/contourRender.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/geodesicContour.d.ts +19 -0
- package/dist/Utils/surfaceAnnotation/geodesicContour.js +43 -0
- package/dist/Utils/surfaceAnnotation/geodesicContour.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/index.d.ts +3 -0
- package/dist/Utils/surfaceAnnotation/index.js +2 -0
- package/dist/Utils/surfaceAnnotation/index.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/pointMarkers.d.ts +7 -0
- package/dist/Utils/surfaceAnnotation/pointMarkers.js +16 -0
- package/dist/Utils/surfaceAnnotation/pointMarkers.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/raycastSurface.d.ts +7 -0
- package/dist/Utils/surfaceAnnotation/raycastSurface.js +27 -0
- package/dist/Utils/surfaceAnnotation/raycastSurface.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/strokeContour.d.ts +19 -0
- package/dist/Utils/surfaceAnnotation/strokeContour.js +35 -0
- package/dist/Utils/surfaceAnnotation/strokeContour.js.map +1 -0
- package/dist/Utils/surfaceAnnotation/types.d.ts +40 -0
- package/dist/Utils/surfaceAnnotation/types.js +26 -0
- package/dist/Utils/surfaceAnnotation/types.js.map +1 -0
- package/dist/bundle.esm.js +3701 -201
- package/dist/bundle.umd.js +3703 -200
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/types/Renderer/baseRenderer.d.ts +3 -0
- package/dist/types/Renderer/copperMSceneRenderer.d.ts +3 -0
- package/dist/types/Renderer/copperRenderer.d.ts +0 -2
- package/dist/types/Scene/copperScene.d.ts +12 -0
- package/dist/types/Utils/segmentation/DrawToolCore.d.ts +3 -0
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +54 -0
- package/dist/types/Utils/segmentation/core/index.d.ts +1 -1
- package/dist/types/Utils/segmentation/core/types.d.ts +11 -1
- package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -0
- package/dist/types/Utils/segmentation/eventRouter/types.d.ts +3 -2
- package/dist/types/Utils/segmentation/tools/AiAssistTool.d.ts +146 -0
- package/dist/types/Utils/surfaceAnnotation/MeshGraph.d.ts +40 -0
- package/dist/types/Utils/surfaceAnnotation/SurfaceAnnotator.d.ts +103 -0
- package/dist/types/Utils/surfaceAnnotation/annotationStore.d.ts +37 -0
- package/dist/types/Utils/surfaceAnnotation/contourRender.d.ts +7 -0
- package/dist/types/Utils/surfaceAnnotation/geodesicContour.d.ts +19 -0
- package/dist/types/Utils/surfaceAnnotation/index.d.ts +3 -0
- package/dist/types/Utils/surfaceAnnotation/pointMarkers.d.ts +7 -0
- package/dist/types/Utils/surfaceAnnotation/raycastSurface.d.ts +7 -0
- package/dist/types/Utils/surfaceAnnotation/strokeContour.d.ts +19 -0
- package/dist/types/Utils/surfaceAnnotation/types.d.ts +40 -0
- package/dist/types/index.d.ts +7 -4
- package/package.json +1 -1
package/dist/bundle.esm.js
CHANGED
|
@@ -18015,7 +18015,7 @@ const mat2array = new Float32Array( 4 );
|
|
|
18015
18015
|
|
|
18016
18016
|
// Flattening for arrays of vectors and matrices
|
|
18017
18017
|
|
|
18018
|
-
function flatten( array, nBlocks, blockSize ) {
|
|
18018
|
+
function flatten$1( array, nBlocks, blockSize ) {
|
|
18019
18019
|
|
|
18020
18020
|
const firstElem = array[ 0 ];
|
|
18021
18021
|
|
|
@@ -18643,7 +18643,7 @@ function setValueV1fArray( gl, v ) {
|
|
|
18643
18643
|
|
|
18644
18644
|
function setValueV2fArray( gl, v ) {
|
|
18645
18645
|
|
|
18646
|
-
const data = flatten( v, this.size, 2 );
|
|
18646
|
+
const data = flatten$1( v, this.size, 2 );
|
|
18647
18647
|
|
|
18648
18648
|
gl.uniform2fv( this.addr, data );
|
|
18649
18649
|
|
|
@@ -18651,7 +18651,7 @@ function setValueV2fArray( gl, v ) {
|
|
|
18651
18651
|
|
|
18652
18652
|
function setValueV3fArray( gl, v ) {
|
|
18653
18653
|
|
|
18654
|
-
const data = flatten( v, this.size, 3 );
|
|
18654
|
+
const data = flatten$1( v, this.size, 3 );
|
|
18655
18655
|
|
|
18656
18656
|
gl.uniform3fv( this.addr, data );
|
|
18657
18657
|
|
|
@@ -18659,7 +18659,7 @@ function setValueV3fArray( gl, v ) {
|
|
|
18659
18659
|
|
|
18660
18660
|
function setValueV4fArray( gl, v ) {
|
|
18661
18661
|
|
|
18662
|
-
const data = flatten( v, this.size, 4 );
|
|
18662
|
+
const data = flatten$1( v, this.size, 4 );
|
|
18663
18663
|
|
|
18664
18664
|
gl.uniform4fv( this.addr, data );
|
|
18665
18665
|
|
|
@@ -18669,7 +18669,7 @@ function setValueV4fArray( gl, v ) {
|
|
|
18669
18669
|
|
|
18670
18670
|
function setValueM2Array( gl, v ) {
|
|
18671
18671
|
|
|
18672
|
-
const data = flatten( v, this.size, 4 );
|
|
18672
|
+
const data = flatten$1( v, this.size, 4 );
|
|
18673
18673
|
|
|
18674
18674
|
gl.uniformMatrix2fv( this.addr, false, data );
|
|
18675
18675
|
|
|
@@ -18677,7 +18677,7 @@ function setValueM2Array( gl, v ) {
|
|
|
18677
18677
|
|
|
18678
18678
|
function setValueM3Array( gl, v ) {
|
|
18679
18679
|
|
|
18680
|
-
const data = flatten( v, this.size, 9 );
|
|
18680
|
+
const data = flatten$1( v, this.size, 9 );
|
|
18681
18681
|
|
|
18682
18682
|
gl.uniformMatrix3fv( this.addr, false, data );
|
|
18683
18683
|
|
|
@@ -18685,7 +18685,7 @@ function setValueM3Array( gl, v ) {
|
|
|
18685
18685
|
|
|
18686
18686
|
function setValueM4Array( gl, v ) {
|
|
18687
18687
|
|
|
18688
|
-
const data = flatten( v, this.size, 16 );
|
|
18688
|
+
const data = flatten$1( v, this.size, 16 );
|
|
18689
18689
|
|
|
18690
18690
|
gl.uniformMatrix4fv( this.addr, false, data );
|
|
18691
18691
|
|
|
@@ -33060,7 +33060,7 @@ const _vStart = /*@__PURE__*/ new Vector3();
|
|
|
33060
33060
|
const _vEnd = /*@__PURE__*/ new Vector3();
|
|
33061
33061
|
|
|
33062
33062
|
const _inverseMatrix$1 = /*@__PURE__*/ new Matrix4();
|
|
33063
|
-
const _ray$1 = /*@__PURE__*/ new Ray();
|
|
33063
|
+
const _ray$1$1 = /*@__PURE__*/ new Ray();
|
|
33064
33064
|
const _sphere$1 = /*@__PURE__*/ new Sphere();
|
|
33065
33065
|
|
|
33066
33066
|
const _intersectPointOnRay = /*@__PURE__*/ new Vector3();
|
|
@@ -33147,7 +33147,7 @@ class Line extends Object3D {
|
|
|
33147
33147
|
//
|
|
33148
33148
|
|
|
33149
33149
|
_inverseMatrix$1.copy( matrixWorld ).invert();
|
|
33150
|
-
_ray$1.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$1 );
|
|
33150
|
+
_ray$1$1.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$1 );
|
|
33151
33151
|
|
|
33152
33152
|
const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
|
|
33153
33153
|
const localThresholdSq = localThreshold * localThreshold;
|
|
@@ -33168,7 +33168,7 @@ class Line extends Object3D {
|
|
|
33168
33168
|
const a = index.getX( i );
|
|
33169
33169
|
const b = index.getX( i + 1 );
|
|
33170
33170
|
|
|
33171
|
-
const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, a, b );
|
|
33171
|
+
const intersect = checkIntersection( this, raycaster, _ray$1$1, localThresholdSq, a, b );
|
|
33172
33172
|
|
|
33173
33173
|
if ( intersect ) {
|
|
33174
33174
|
|
|
@@ -33183,7 +33183,7 @@ class Line extends Object3D {
|
|
|
33183
33183
|
const a = index.getX( end - 1 );
|
|
33184
33184
|
const b = index.getX( start );
|
|
33185
33185
|
|
|
33186
|
-
const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, a, b );
|
|
33186
|
+
const intersect = checkIntersection( this, raycaster, _ray$1$1, localThresholdSq, a, b );
|
|
33187
33187
|
|
|
33188
33188
|
if ( intersect ) {
|
|
33189
33189
|
|
|
@@ -33200,7 +33200,7 @@ class Line extends Object3D {
|
|
|
33200
33200
|
|
|
33201
33201
|
for ( let i = start, l = end - 1; i < l; i += step ) {
|
|
33202
33202
|
|
|
33203
|
-
const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, i, i + 1 );
|
|
33203
|
+
const intersect = checkIntersection( this, raycaster, _ray$1$1, localThresholdSq, i, i + 1 );
|
|
33204
33204
|
|
|
33205
33205
|
if ( intersect ) {
|
|
33206
33206
|
|
|
@@ -33212,7 +33212,7 @@ class Line extends Object3D {
|
|
|
33212
33212
|
|
|
33213
33213
|
if ( this.isLineLoop ) {
|
|
33214
33214
|
|
|
33215
|
-
const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, end - 1, start );
|
|
33215
|
+
const intersect = checkIntersection( this, raycaster, _ray$1$1, localThresholdSq, end - 1, start );
|
|
33216
33216
|
|
|
33217
33217
|
if ( intersect ) {
|
|
33218
33218
|
|
|
@@ -33292,8 +33292,8 @@ function checkIntersection( object, raycaster, ray, thresholdSq, a, b ) {
|
|
|
33292
33292
|
|
|
33293
33293
|
}
|
|
33294
33294
|
|
|
33295
|
-
const _start = /*@__PURE__*/ new Vector3();
|
|
33296
|
-
const _end = /*@__PURE__*/ new Vector3();
|
|
33295
|
+
const _start$1 = /*@__PURE__*/ new Vector3();
|
|
33296
|
+
const _end$1 = /*@__PURE__*/ new Vector3();
|
|
33297
33297
|
|
|
33298
33298
|
class LineSegments extends Line {
|
|
33299
33299
|
|
|
@@ -33320,11 +33320,11 @@ class LineSegments extends Line {
|
|
|
33320
33320
|
|
|
33321
33321
|
for ( let i = 0, l = positionAttribute.count; i < l; i += 2 ) {
|
|
33322
33322
|
|
|
33323
|
-
_start.fromBufferAttribute( positionAttribute, i );
|
|
33324
|
-
_end.fromBufferAttribute( positionAttribute, i + 1 );
|
|
33323
|
+
_start$1.fromBufferAttribute( positionAttribute, i );
|
|
33324
|
+
_end$1.fromBufferAttribute( positionAttribute, i + 1 );
|
|
33325
33325
|
|
|
33326
33326
|
lineDistances[ i ] = ( i === 0 ) ? 0 : lineDistances[ i - 1 ];
|
|
33327
|
-
lineDistances[ i + 1 ] = lineDistances[ i ] + _start.distanceTo( _end );
|
|
33327
|
+
lineDistances[ i + 1 ] = lineDistances[ i ] + _start$1.distanceTo( _end$1 );
|
|
33328
33328
|
|
|
33329
33329
|
}
|
|
33330
33330
|
|
|
@@ -33408,7 +33408,7 @@ class PointsMaterial extends Material {
|
|
|
33408
33408
|
|
|
33409
33409
|
const _inverseMatrix = /*@__PURE__*/ new Matrix4();
|
|
33410
33410
|
const _ray$4 = /*@__PURE__*/ new Ray();
|
|
33411
|
-
const _sphere = /*@__PURE__*/ new Sphere();
|
|
33411
|
+
const _sphere$2 = /*@__PURE__*/ new Sphere();
|
|
33412
33412
|
const _position$2 = /*@__PURE__*/ new Vector3();
|
|
33413
33413
|
|
|
33414
33414
|
class Points extends Object3D {
|
|
@@ -33450,11 +33450,11 @@ class Points extends Object3D {
|
|
|
33450
33450
|
|
|
33451
33451
|
if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
|
|
33452
33452
|
|
|
33453
|
-
_sphere.copy( geometry.boundingSphere );
|
|
33454
|
-
_sphere.applyMatrix4( matrixWorld );
|
|
33455
|
-
_sphere.radius += threshold;
|
|
33453
|
+
_sphere$2.copy( geometry.boundingSphere );
|
|
33454
|
+
_sphere$2.applyMatrix4( matrixWorld );
|
|
33455
|
+
_sphere$2.radius += threshold;
|
|
33456
33456
|
|
|
33457
|
-
if ( raycaster.ray.intersectsSphere( _sphere ) === false ) return;
|
|
33457
|
+
if ( raycaster.ray.intersectsSphere( _sphere$2 ) === false ) return;
|
|
33458
33458
|
|
|
33459
33459
|
//
|
|
33460
33460
|
|
|
@@ -33754,6 +33754,279 @@ class RingGeometry extends BufferGeometry {
|
|
|
33754
33754
|
|
|
33755
33755
|
}
|
|
33756
33756
|
|
|
33757
|
+
class SphereGeometry extends BufferGeometry {
|
|
33758
|
+
|
|
33759
|
+
constructor( radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI ) {
|
|
33760
|
+
|
|
33761
|
+
super();
|
|
33762
|
+
|
|
33763
|
+
this.type = 'SphereGeometry';
|
|
33764
|
+
|
|
33765
|
+
this.parameters = {
|
|
33766
|
+
radius: radius,
|
|
33767
|
+
widthSegments: widthSegments,
|
|
33768
|
+
heightSegments: heightSegments,
|
|
33769
|
+
phiStart: phiStart,
|
|
33770
|
+
phiLength: phiLength,
|
|
33771
|
+
thetaStart: thetaStart,
|
|
33772
|
+
thetaLength: thetaLength
|
|
33773
|
+
};
|
|
33774
|
+
|
|
33775
|
+
widthSegments = Math.max( 3, Math.floor( widthSegments ) );
|
|
33776
|
+
heightSegments = Math.max( 2, Math.floor( heightSegments ) );
|
|
33777
|
+
|
|
33778
|
+
const thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );
|
|
33779
|
+
|
|
33780
|
+
let index = 0;
|
|
33781
|
+
const grid = [];
|
|
33782
|
+
|
|
33783
|
+
const vertex = new Vector3();
|
|
33784
|
+
const normal = new Vector3();
|
|
33785
|
+
|
|
33786
|
+
// buffers
|
|
33787
|
+
|
|
33788
|
+
const indices = [];
|
|
33789
|
+
const vertices = [];
|
|
33790
|
+
const normals = [];
|
|
33791
|
+
const uvs = [];
|
|
33792
|
+
|
|
33793
|
+
// generate vertices, normals and uvs
|
|
33794
|
+
|
|
33795
|
+
for ( let iy = 0; iy <= heightSegments; iy ++ ) {
|
|
33796
|
+
|
|
33797
|
+
const verticesRow = [];
|
|
33798
|
+
|
|
33799
|
+
const v = iy / heightSegments;
|
|
33800
|
+
|
|
33801
|
+
// special case for the poles
|
|
33802
|
+
|
|
33803
|
+
let uOffset = 0;
|
|
33804
|
+
|
|
33805
|
+
if ( iy === 0 && thetaStart === 0 ) {
|
|
33806
|
+
|
|
33807
|
+
uOffset = 0.5 / widthSegments;
|
|
33808
|
+
|
|
33809
|
+
} else if ( iy === heightSegments && thetaEnd === Math.PI ) {
|
|
33810
|
+
|
|
33811
|
+
uOffset = - 0.5 / widthSegments;
|
|
33812
|
+
|
|
33813
|
+
}
|
|
33814
|
+
|
|
33815
|
+
for ( let ix = 0; ix <= widthSegments; ix ++ ) {
|
|
33816
|
+
|
|
33817
|
+
const u = ix / widthSegments;
|
|
33818
|
+
|
|
33819
|
+
// vertex
|
|
33820
|
+
|
|
33821
|
+
vertex.x = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
|
|
33822
|
+
vertex.y = radius * Math.cos( thetaStart + v * thetaLength );
|
|
33823
|
+
vertex.z = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
|
|
33824
|
+
|
|
33825
|
+
vertices.push( vertex.x, vertex.y, vertex.z );
|
|
33826
|
+
|
|
33827
|
+
// normal
|
|
33828
|
+
|
|
33829
|
+
normal.copy( vertex ).normalize();
|
|
33830
|
+
normals.push( normal.x, normal.y, normal.z );
|
|
33831
|
+
|
|
33832
|
+
// uv
|
|
33833
|
+
|
|
33834
|
+
uvs.push( u + uOffset, 1 - v );
|
|
33835
|
+
|
|
33836
|
+
verticesRow.push( index ++ );
|
|
33837
|
+
|
|
33838
|
+
}
|
|
33839
|
+
|
|
33840
|
+
grid.push( verticesRow );
|
|
33841
|
+
|
|
33842
|
+
}
|
|
33843
|
+
|
|
33844
|
+
// indices
|
|
33845
|
+
|
|
33846
|
+
for ( let iy = 0; iy < heightSegments; iy ++ ) {
|
|
33847
|
+
|
|
33848
|
+
for ( let ix = 0; ix < widthSegments; ix ++ ) {
|
|
33849
|
+
|
|
33850
|
+
const a = grid[ iy ][ ix + 1 ];
|
|
33851
|
+
const b = grid[ iy ][ ix ];
|
|
33852
|
+
const c = grid[ iy + 1 ][ ix ];
|
|
33853
|
+
const d = grid[ iy + 1 ][ ix + 1 ];
|
|
33854
|
+
|
|
33855
|
+
if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
|
|
33856
|
+
if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );
|
|
33857
|
+
|
|
33858
|
+
}
|
|
33859
|
+
|
|
33860
|
+
}
|
|
33861
|
+
|
|
33862
|
+
// build geometry
|
|
33863
|
+
|
|
33864
|
+
this.setIndex( indices );
|
|
33865
|
+
this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
|
|
33866
|
+
this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
|
|
33867
|
+
this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
|
|
33868
|
+
|
|
33869
|
+
}
|
|
33870
|
+
|
|
33871
|
+
copy( source ) {
|
|
33872
|
+
|
|
33873
|
+
super.copy( source );
|
|
33874
|
+
|
|
33875
|
+
this.parameters = Object.assign( {}, source.parameters );
|
|
33876
|
+
|
|
33877
|
+
return this;
|
|
33878
|
+
|
|
33879
|
+
}
|
|
33880
|
+
|
|
33881
|
+
static fromJSON( data ) {
|
|
33882
|
+
|
|
33883
|
+
return new SphereGeometry( data.radius, data.widthSegments, data.heightSegments, data.phiStart, data.phiLength, data.thetaStart, data.thetaLength );
|
|
33884
|
+
|
|
33885
|
+
}
|
|
33886
|
+
|
|
33887
|
+
}
|
|
33888
|
+
|
|
33889
|
+
class WireframeGeometry extends BufferGeometry {
|
|
33890
|
+
|
|
33891
|
+
constructor( geometry = null ) {
|
|
33892
|
+
|
|
33893
|
+
super();
|
|
33894
|
+
|
|
33895
|
+
this.type = 'WireframeGeometry';
|
|
33896
|
+
|
|
33897
|
+
this.parameters = {
|
|
33898
|
+
geometry: geometry
|
|
33899
|
+
};
|
|
33900
|
+
|
|
33901
|
+
if ( geometry !== null ) {
|
|
33902
|
+
|
|
33903
|
+
// buffer
|
|
33904
|
+
|
|
33905
|
+
const vertices = [];
|
|
33906
|
+
const edges = new Set();
|
|
33907
|
+
|
|
33908
|
+
// helper variables
|
|
33909
|
+
|
|
33910
|
+
const start = new Vector3();
|
|
33911
|
+
const end = new Vector3();
|
|
33912
|
+
|
|
33913
|
+
if ( geometry.index !== null ) {
|
|
33914
|
+
|
|
33915
|
+
// indexed BufferGeometry
|
|
33916
|
+
|
|
33917
|
+
const position = geometry.attributes.position;
|
|
33918
|
+
const indices = geometry.index;
|
|
33919
|
+
let groups = geometry.groups;
|
|
33920
|
+
|
|
33921
|
+
if ( groups.length === 0 ) {
|
|
33922
|
+
|
|
33923
|
+
groups = [ { start: 0, count: indices.count, materialIndex: 0 } ];
|
|
33924
|
+
|
|
33925
|
+
}
|
|
33926
|
+
|
|
33927
|
+
// create a data structure that contains all edges without duplicates
|
|
33928
|
+
|
|
33929
|
+
for ( let o = 0, ol = groups.length; o < ol; ++ o ) {
|
|
33930
|
+
|
|
33931
|
+
const group = groups[ o ];
|
|
33932
|
+
|
|
33933
|
+
const groupStart = group.start;
|
|
33934
|
+
const groupCount = group.count;
|
|
33935
|
+
|
|
33936
|
+
for ( let i = groupStart, l = ( groupStart + groupCount ); i < l; i += 3 ) {
|
|
33937
|
+
|
|
33938
|
+
for ( let j = 0; j < 3; j ++ ) {
|
|
33939
|
+
|
|
33940
|
+
const index1 = indices.getX( i + j );
|
|
33941
|
+
const index2 = indices.getX( i + ( j + 1 ) % 3 );
|
|
33942
|
+
|
|
33943
|
+
start.fromBufferAttribute( position, index1 );
|
|
33944
|
+
end.fromBufferAttribute( position, index2 );
|
|
33945
|
+
|
|
33946
|
+
if ( isUniqueEdge( start, end, edges ) === true ) {
|
|
33947
|
+
|
|
33948
|
+
vertices.push( start.x, start.y, start.z );
|
|
33949
|
+
vertices.push( end.x, end.y, end.z );
|
|
33950
|
+
|
|
33951
|
+
}
|
|
33952
|
+
|
|
33953
|
+
}
|
|
33954
|
+
|
|
33955
|
+
}
|
|
33956
|
+
|
|
33957
|
+
}
|
|
33958
|
+
|
|
33959
|
+
} else {
|
|
33960
|
+
|
|
33961
|
+
// non-indexed BufferGeometry
|
|
33962
|
+
|
|
33963
|
+
const position = geometry.attributes.position;
|
|
33964
|
+
|
|
33965
|
+
for ( let i = 0, l = ( position.count / 3 ); i < l; i ++ ) {
|
|
33966
|
+
|
|
33967
|
+
for ( let j = 0; j < 3; j ++ ) {
|
|
33968
|
+
|
|
33969
|
+
// three edges per triangle, an edge is represented as (index1, index2)
|
|
33970
|
+
// e.g. the first triangle has the following edges: (0,1),(1,2),(2,0)
|
|
33971
|
+
|
|
33972
|
+
const index1 = 3 * i + j;
|
|
33973
|
+
const index2 = 3 * i + ( ( j + 1 ) % 3 );
|
|
33974
|
+
|
|
33975
|
+
start.fromBufferAttribute( position, index1 );
|
|
33976
|
+
end.fromBufferAttribute( position, index2 );
|
|
33977
|
+
|
|
33978
|
+
if ( isUniqueEdge( start, end, edges ) === true ) {
|
|
33979
|
+
|
|
33980
|
+
vertices.push( start.x, start.y, start.z );
|
|
33981
|
+
vertices.push( end.x, end.y, end.z );
|
|
33982
|
+
|
|
33983
|
+
}
|
|
33984
|
+
|
|
33985
|
+
}
|
|
33986
|
+
|
|
33987
|
+
}
|
|
33988
|
+
|
|
33989
|
+
}
|
|
33990
|
+
|
|
33991
|
+
// build geometry
|
|
33992
|
+
|
|
33993
|
+
this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
|
|
33994
|
+
|
|
33995
|
+
}
|
|
33996
|
+
|
|
33997
|
+
}
|
|
33998
|
+
|
|
33999
|
+
copy( source ) {
|
|
34000
|
+
|
|
34001
|
+
super.copy( source );
|
|
34002
|
+
|
|
34003
|
+
this.parameters = Object.assign( {}, source.parameters );
|
|
34004
|
+
|
|
34005
|
+
return this;
|
|
34006
|
+
|
|
34007
|
+
}
|
|
34008
|
+
|
|
34009
|
+
}
|
|
34010
|
+
|
|
34011
|
+
function isUniqueEdge( start, end, edges ) {
|
|
34012
|
+
|
|
34013
|
+
const hash1 = `${start.x},${start.y},${start.z}-${end.x},${end.y},${end.z}`;
|
|
34014
|
+
const hash2 = `${end.x},${end.y},${end.z}-${start.x},${start.y},${start.z}`; // coincident edge
|
|
34015
|
+
|
|
34016
|
+
if ( edges.has( hash1 ) === true || edges.has( hash2 ) === true ) {
|
|
34017
|
+
|
|
34018
|
+
return false;
|
|
34019
|
+
|
|
34020
|
+
} else {
|
|
34021
|
+
|
|
34022
|
+
edges.add( hash1 );
|
|
34023
|
+
edges.add( hash2 );
|
|
34024
|
+
return true;
|
|
34025
|
+
|
|
34026
|
+
}
|
|
34027
|
+
|
|
34028
|
+
}
|
|
34029
|
+
|
|
33757
34030
|
class RawShaderMaterial extends ShaderMaterial {
|
|
33758
34031
|
|
|
33759
34032
|
static get type() {
|
|
@@ -37262,6 +37535,43 @@ class LoaderUtils {
|
|
|
37262
37535
|
|
|
37263
37536
|
}
|
|
37264
37537
|
|
|
37538
|
+
class InstancedBufferGeometry extends BufferGeometry {
|
|
37539
|
+
|
|
37540
|
+
constructor() {
|
|
37541
|
+
|
|
37542
|
+
super();
|
|
37543
|
+
|
|
37544
|
+
this.isInstancedBufferGeometry = true;
|
|
37545
|
+
|
|
37546
|
+
this.type = 'InstancedBufferGeometry';
|
|
37547
|
+
this.instanceCount = Infinity;
|
|
37548
|
+
|
|
37549
|
+
}
|
|
37550
|
+
|
|
37551
|
+
copy( source ) {
|
|
37552
|
+
|
|
37553
|
+
super.copy( source );
|
|
37554
|
+
|
|
37555
|
+
this.instanceCount = source.instanceCount;
|
|
37556
|
+
|
|
37557
|
+
return this;
|
|
37558
|
+
|
|
37559
|
+
}
|
|
37560
|
+
|
|
37561
|
+
toJSON() {
|
|
37562
|
+
|
|
37563
|
+
const data = super.toJSON();
|
|
37564
|
+
|
|
37565
|
+
data.instanceCount = this.instanceCount;
|
|
37566
|
+
|
|
37567
|
+
data.isInstancedBufferGeometry = true;
|
|
37568
|
+
|
|
37569
|
+
return data;
|
|
37570
|
+
|
|
37571
|
+
}
|
|
37572
|
+
|
|
37573
|
+
}
|
|
37574
|
+
|
|
37265
37575
|
class ImageBitmapLoader extends Loader {
|
|
37266
37576
|
|
|
37267
37577
|
constructor( manager ) {
|
|
@@ -39940,6 +40250,51 @@ class AnimationMixer extends EventDispatcher {
|
|
|
39940
40250
|
|
|
39941
40251
|
}
|
|
39942
40252
|
|
|
40253
|
+
class InstancedInterleavedBuffer extends InterleavedBuffer {
|
|
40254
|
+
|
|
40255
|
+
constructor( array, stride, meshPerAttribute = 1 ) {
|
|
40256
|
+
|
|
40257
|
+
super( array, stride );
|
|
40258
|
+
|
|
40259
|
+
this.isInstancedInterleavedBuffer = true;
|
|
40260
|
+
|
|
40261
|
+
this.meshPerAttribute = meshPerAttribute;
|
|
40262
|
+
|
|
40263
|
+
}
|
|
40264
|
+
|
|
40265
|
+
copy( source ) {
|
|
40266
|
+
|
|
40267
|
+
super.copy( source );
|
|
40268
|
+
|
|
40269
|
+
this.meshPerAttribute = source.meshPerAttribute;
|
|
40270
|
+
|
|
40271
|
+
return this;
|
|
40272
|
+
|
|
40273
|
+
}
|
|
40274
|
+
|
|
40275
|
+
clone( data ) {
|
|
40276
|
+
|
|
40277
|
+
const ib = super.clone( data );
|
|
40278
|
+
|
|
40279
|
+
ib.meshPerAttribute = this.meshPerAttribute;
|
|
40280
|
+
|
|
40281
|
+
return ib;
|
|
40282
|
+
|
|
40283
|
+
}
|
|
40284
|
+
|
|
40285
|
+
toJSON( data ) {
|
|
40286
|
+
|
|
40287
|
+
const json = super.toJSON( data );
|
|
40288
|
+
|
|
40289
|
+
json.isInstancedInterleavedBuffer = true;
|
|
40290
|
+
json.meshPerAttribute = this.meshPerAttribute;
|
|
40291
|
+
|
|
40292
|
+
return json;
|
|
40293
|
+
|
|
40294
|
+
}
|
|
40295
|
+
|
|
40296
|
+
}
|
|
40297
|
+
|
|
39943
40298
|
const _matrix = /*@__PURE__*/ new Matrix4();
|
|
39944
40299
|
|
|
39945
40300
|
class Raycaster {
|
|
@@ -40145,7 +40500,118 @@ class Spherical {
|
|
|
40145
40500
|
|
|
40146
40501
|
}
|
|
40147
40502
|
|
|
40148
|
-
const
|
|
40503
|
+
const _startP = /*@__PURE__*/ new Vector3();
|
|
40504
|
+
const _startEnd = /*@__PURE__*/ new Vector3();
|
|
40505
|
+
|
|
40506
|
+
class Line3 {
|
|
40507
|
+
|
|
40508
|
+
constructor( start = new Vector3(), end = new Vector3() ) {
|
|
40509
|
+
|
|
40510
|
+
this.start = start;
|
|
40511
|
+
this.end = end;
|
|
40512
|
+
|
|
40513
|
+
}
|
|
40514
|
+
|
|
40515
|
+
set( start, end ) {
|
|
40516
|
+
|
|
40517
|
+
this.start.copy( start );
|
|
40518
|
+
this.end.copy( end );
|
|
40519
|
+
|
|
40520
|
+
return this;
|
|
40521
|
+
|
|
40522
|
+
}
|
|
40523
|
+
|
|
40524
|
+
copy( line ) {
|
|
40525
|
+
|
|
40526
|
+
this.start.copy( line.start );
|
|
40527
|
+
this.end.copy( line.end );
|
|
40528
|
+
|
|
40529
|
+
return this;
|
|
40530
|
+
|
|
40531
|
+
}
|
|
40532
|
+
|
|
40533
|
+
getCenter( target ) {
|
|
40534
|
+
|
|
40535
|
+
return target.addVectors( this.start, this.end ).multiplyScalar( 0.5 );
|
|
40536
|
+
|
|
40537
|
+
}
|
|
40538
|
+
|
|
40539
|
+
delta( target ) {
|
|
40540
|
+
|
|
40541
|
+
return target.subVectors( this.end, this.start );
|
|
40542
|
+
|
|
40543
|
+
}
|
|
40544
|
+
|
|
40545
|
+
distanceSq() {
|
|
40546
|
+
|
|
40547
|
+
return this.start.distanceToSquared( this.end );
|
|
40548
|
+
|
|
40549
|
+
}
|
|
40550
|
+
|
|
40551
|
+
distance() {
|
|
40552
|
+
|
|
40553
|
+
return this.start.distanceTo( this.end );
|
|
40554
|
+
|
|
40555
|
+
}
|
|
40556
|
+
|
|
40557
|
+
at( t, target ) {
|
|
40558
|
+
|
|
40559
|
+
return this.delta( target ).multiplyScalar( t ).add( this.start );
|
|
40560
|
+
|
|
40561
|
+
}
|
|
40562
|
+
|
|
40563
|
+
closestPointToPointParameter( point, clampToLine ) {
|
|
40564
|
+
|
|
40565
|
+
_startP.subVectors( point, this.start );
|
|
40566
|
+
_startEnd.subVectors( this.end, this.start );
|
|
40567
|
+
|
|
40568
|
+
const startEnd2 = _startEnd.dot( _startEnd );
|
|
40569
|
+
const startEnd_startP = _startEnd.dot( _startP );
|
|
40570
|
+
|
|
40571
|
+
let t = startEnd_startP / startEnd2;
|
|
40572
|
+
|
|
40573
|
+
if ( clampToLine ) {
|
|
40574
|
+
|
|
40575
|
+
t = clamp( t, 0, 1 );
|
|
40576
|
+
|
|
40577
|
+
}
|
|
40578
|
+
|
|
40579
|
+
return t;
|
|
40580
|
+
|
|
40581
|
+
}
|
|
40582
|
+
|
|
40583
|
+
closestPointToPoint( point, clampToLine, target ) {
|
|
40584
|
+
|
|
40585
|
+
const t = this.closestPointToPointParameter( point, clampToLine );
|
|
40586
|
+
|
|
40587
|
+
return this.delta( target ).multiplyScalar( t ).add( this.start );
|
|
40588
|
+
|
|
40589
|
+
}
|
|
40590
|
+
|
|
40591
|
+
applyMatrix4( matrix ) {
|
|
40592
|
+
|
|
40593
|
+
this.start.applyMatrix4( matrix );
|
|
40594
|
+
this.end.applyMatrix4( matrix );
|
|
40595
|
+
|
|
40596
|
+
return this;
|
|
40597
|
+
|
|
40598
|
+
}
|
|
40599
|
+
|
|
40600
|
+
equals( line ) {
|
|
40601
|
+
|
|
40602
|
+
return line.start.equals( this.start ) && line.end.equals( this.end );
|
|
40603
|
+
|
|
40604
|
+
}
|
|
40605
|
+
|
|
40606
|
+
clone() {
|
|
40607
|
+
|
|
40608
|
+
return new this.constructor().copy( this );
|
|
40609
|
+
|
|
40610
|
+
}
|
|
40611
|
+
|
|
40612
|
+
}
|
|
40613
|
+
|
|
40614
|
+
const _box$5 = /*@__PURE__*/ new Box3();
|
|
40149
40615
|
|
|
40150
40616
|
class BoxHelper extends LineSegments {
|
|
40151
40617
|
|
|
@@ -40179,14 +40645,14 @@ class BoxHelper extends LineSegments {
|
|
|
40179
40645
|
|
|
40180
40646
|
if ( this.object !== undefined ) {
|
|
40181
40647
|
|
|
40182
|
-
_box.setFromObject( this.object );
|
|
40648
|
+
_box$5.setFromObject( this.object );
|
|
40183
40649
|
|
|
40184
40650
|
}
|
|
40185
40651
|
|
|
40186
|
-
if ( _box.isEmpty() ) return;
|
|
40652
|
+
if ( _box$5.isEmpty() ) return;
|
|
40187
40653
|
|
|
40188
|
-
const min = _box.min;
|
|
40189
|
-
const max = _box.max;
|
|
40654
|
+
const min = _box$5.min;
|
|
40655
|
+
const max = _box$5.max;
|
|
40190
40656
|
|
|
40191
40657
|
/*
|
|
40192
40658
|
5____4
|
|
@@ -47507,7 +47973,7 @@ class Copper3dTrackballControls extends EventDispatcher {
|
|
|
47507
47973
|
const _changeEvent$1 = { type: 'change' };
|
|
47508
47974
|
const _startEvent$1 = { type: 'start' };
|
|
47509
47975
|
const _endEvent$1 = { type: 'end' };
|
|
47510
|
-
const _ray = new Ray();
|
|
47976
|
+
const _ray$1 = new Ray();
|
|
47511
47977
|
const _plane = new Plane();
|
|
47512
47978
|
const _TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );
|
|
47513
47979
|
|
|
@@ -47950,19 +48416,19 @@ class OrbitControls extends Controls$1 {
|
|
|
47950
48416
|
} else {
|
|
47951
48417
|
|
|
47952
48418
|
// get the ray and translation plane to compute target
|
|
47953
|
-
_ray.origin.copy( this.object.position );
|
|
47954
|
-
_ray.direction.set( 0, 0, - 1 ).transformDirection( this.object.matrix );
|
|
48419
|
+
_ray$1.origin.copy( this.object.position );
|
|
48420
|
+
_ray$1.direction.set( 0, 0, - 1 ).transformDirection( this.object.matrix );
|
|
47955
48421
|
|
|
47956
48422
|
// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
|
|
47957
48423
|
// extremely large values
|
|
47958
|
-
if ( Math.abs( this.object.up.dot( _ray.direction ) ) < _TILT_LIMIT ) {
|
|
48424
|
+
if ( Math.abs( this.object.up.dot( _ray$1.direction ) ) < _TILT_LIMIT ) {
|
|
47959
48425
|
|
|
47960
48426
|
this.object.lookAt( this.target );
|
|
47961
48427
|
|
|
47962
48428
|
} else {
|
|
47963
48429
|
|
|
47964
48430
|
_plane.setFromNormalAndCoplanarPoint( this.object.up, this.target );
|
|
47965
|
-
_ray.intersectPlane( _plane, this.target );
|
|
48431
|
+
_ray$1.intersectPlane( _plane, this.target );
|
|
47966
48432
|
|
|
47967
48433
|
}
|
|
47968
48434
|
|
|
@@ -50086,6 +50552,7 @@ var Stats$1 = Stats;
|
|
|
50086
50552
|
class baseRenderer {
|
|
50087
50553
|
constructor(container, options) {
|
|
50088
50554
|
var _a, _b, _c, _d, _e;
|
|
50555
|
+
this.running = true;
|
|
50089
50556
|
this.visualCtrls = [];
|
|
50090
50557
|
this.container = container;
|
|
50091
50558
|
this.options = options;
|
|
@@ -50186,6 +50653,25 @@ class baseRenderer {
|
|
|
50186
50653
|
setClearColor(clearColor = 0x000000, alpha = 0) {
|
|
50187
50654
|
this.renderer.setClearColor(clearColor, alpha);
|
|
50188
50655
|
}
|
|
50656
|
+
// Stop the render loop (cancels the requestAnimationFrame chain).
|
|
50657
|
+
stop() {
|
|
50658
|
+
this.running = false;
|
|
50659
|
+
}
|
|
50660
|
+
// Full teardown: stop the loop, free the GPU resources, release the WebGL context
|
|
50661
|
+
// (browsers cap live contexts ~16), and remove the canvas. Call on page unmount.
|
|
50662
|
+
dispose() {
|
|
50663
|
+
var _a, _b;
|
|
50664
|
+
this.running = false;
|
|
50665
|
+
try {
|
|
50666
|
+
(_a = this.pmremGenerator) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
50667
|
+
this.renderer.dispose();
|
|
50668
|
+
this.renderer.forceContextLoss();
|
|
50669
|
+
}
|
|
50670
|
+
catch (e) {
|
|
50671
|
+
/* ignore */
|
|
50672
|
+
}
|
|
50673
|
+
(_b = this.renderer.domElement) === null || _b === void 0 ? void 0 : _b.remove();
|
|
50674
|
+
}
|
|
50189
50675
|
addGui() {
|
|
50190
50676
|
var _a, _b, _c;
|
|
50191
50677
|
const gui = (this.gui = new GUI$1({
|
|
@@ -50303,6 +50789,169 @@ class baseRenderer {
|
|
|
50303
50789
|
}
|
|
50304
50790
|
}
|
|
50305
50791
|
|
|
50792
|
+
/**
|
|
50793
|
+
* @param {BufferGeometry} geometry
|
|
50794
|
+
* @param {number} tolerance
|
|
50795
|
+
* @return {BufferGeometry}
|
|
50796
|
+
*/
|
|
50797
|
+
function mergeVertices( geometry, tolerance = 1e-4 ) {
|
|
50798
|
+
|
|
50799
|
+
tolerance = Math.max( tolerance, Number.EPSILON );
|
|
50800
|
+
|
|
50801
|
+
// Generate an index buffer if the geometry doesn't have one, or optimize it
|
|
50802
|
+
// if it's already available.
|
|
50803
|
+
const hashToIndex = {};
|
|
50804
|
+
const indices = geometry.getIndex();
|
|
50805
|
+
const positions = geometry.getAttribute( 'position' );
|
|
50806
|
+
const vertexCount = indices ? indices.count : positions.count;
|
|
50807
|
+
|
|
50808
|
+
// next value for triangle indices
|
|
50809
|
+
let nextIndex = 0;
|
|
50810
|
+
|
|
50811
|
+
// attributes and new attribute arrays
|
|
50812
|
+
const attributeNames = Object.keys( geometry.attributes );
|
|
50813
|
+
const tmpAttributes = {};
|
|
50814
|
+
const tmpMorphAttributes = {};
|
|
50815
|
+
const newIndices = [];
|
|
50816
|
+
const getters = [ 'getX', 'getY', 'getZ', 'getW' ];
|
|
50817
|
+
const setters = [ 'setX', 'setY', 'setZ', 'setW' ];
|
|
50818
|
+
|
|
50819
|
+
// Initialize the arrays, allocating space conservatively. Extra
|
|
50820
|
+
// space will be trimmed in the last step.
|
|
50821
|
+
for ( let i = 0, l = attributeNames.length; i < l; i ++ ) {
|
|
50822
|
+
|
|
50823
|
+
const name = attributeNames[ i ];
|
|
50824
|
+
const attr = geometry.attributes[ name ];
|
|
50825
|
+
|
|
50826
|
+
tmpAttributes[ name ] = new attr.constructor(
|
|
50827
|
+
new attr.array.constructor( attr.count * attr.itemSize ),
|
|
50828
|
+
attr.itemSize,
|
|
50829
|
+
attr.normalized
|
|
50830
|
+
);
|
|
50831
|
+
|
|
50832
|
+
const morphAttributes = geometry.morphAttributes[ name ];
|
|
50833
|
+
if ( morphAttributes ) {
|
|
50834
|
+
|
|
50835
|
+
if ( ! tmpMorphAttributes[ name ] ) tmpMorphAttributes[ name ] = [];
|
|
50836
|
+
morphAttributes.forEach( ( morphAttr, i ) => {
|
|
50837
|
+
|
|
50838
|
+
const array = new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize );
|
|
50839
|
+
tmpMorphAttributes[ name ][ i ] = new morphAttr.constructor( array, morphAttr.itemSize, morphAttr.normalized );
|
|
50840
|
+
|
|
50841
|
+
} );
|
|
50842
|
+
|
|
50843
|
+
}
|
|
50844
|
+
|
|
50845
|
+
}
|
|
50846
|
+
|
|
50847
|
+
// convert the error tolerance to an amount of decimal places to truncate to
|
|
50848
|
+
const halfTolerance = tolerance * 0.5;
|
|
50849
|
+
const exponent = Math.log10( 1 / tolerance );
|
|
50850
|
+
const hashMultiplier = Math.pow( 10, exponent );
|
|
50851
|
+
const hashAdditive = halfTolerance * hashMultiplier;
|
|
50852
|
+
for ( let i = 0; i < vertexCount; i ++ ) {
|
|
50853
|
+
|
|
50854
|
+
const index = indices ? indices.getX( i ) : i;
|
|
50855
|
+
|
|
50856
|
+
// Generate a hash for the vertex attributes at the current index 'i'
|
|
50857
|
+
let hash = '';
|
|
50858
|
+
for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {
|
|
50859
|
+
|
|
50860
|
+
const name = attributeNames[ j ];
|
|
50861
|
+
const attribute = geometry.getAttribute( name );
|
|
50862
|
+
const itemSize = attribute.itemSize;
|
|
50863
|
+
|
|
50864
|
+
for ( let k = 0; k < itemSize; k ++ ) {
|
|
50865
|
+
|
|
50866
|
+
// double tilde truncates the decimal value
|
|
50867
|
+
hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * hashMultiplier + hashAdditive ) },`;
|
|
50868
|
+
|
|
50869
|
+
}
|
|
50870
|
+
|
|
50871
|
+
}
|
|
50872
|
+
|
|
50873
|
+
// Add another reference to the vertex if it's already
|
|
50874
|
+
// used by another index
|
|
50875
|
+
if ( hash in hashToIndex ) {
|
|
50876
|
+
|
|
50877
|
+
newIndices.push( hashToIndex[ hash ] );
|
|
50878
|
+
|
|
50879
|
+
} else {
|
|
50880
|
+
|
|
50881
|
+
// copy data to the new index in the temporary attributes
|
|
50882
|
+
for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {
|
|
50883
|
+
|
|
50884
|
+
const name = attributeNames[ j ];
|
|
50885
|
+
const attribute = geometry.getAttribute( name );
|
|
50886
|
+
const morphAttributes = geometry.morphAttributes[ name ];
|
|
50887
|
+
const itemSize = attribute.itemSize;
|
|
50888
|
+
const newArray = tmpAttributes[ name ];
|
|
50889
|
+
const newMorphArrays = tmpMorphAttributes[ name ];
|
|
50890
|
+
|
|
50891
|
+
for ( let k = 0; k < itemSize; k ++ ) {
|
|
50892
|
+
|
|
50893
|
+
const getterFunc = getters[ k ];
|
|
50894
|
+
const setterFunc = setters[ k ];
|
|
50895
|
+
newArray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) );
|
|
50896
|
+
|
|
50897
|
+
if ( morphAttributes ) {
|
|
50898
|
+
|
|
50899
|
+
for ( let m = 0, ml = morphAttributes.length; m < ml; m ++ ) {
|
|
50900
|
+
|
|
50901
|
+
newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttributes[ m ][ getterFunc ]( index ) );
|
|
50902
|
+
|
|
50903
|
+
}
|
|
50904
|
+
|
|
50905
|
+
}
|
|
50906
|
+
|
|
50907
|
+
}
|
|
50908
|
+
|
|
50909
|
+
}
|
|
50910
|
+
|
|
50911
|
+
hashToIndex[ hash ] = nextIndex;
|
|
50912
|
+
newIndices.push( nextIndex );
|
|
50913
|
+
nextIndex ++;
|
|
50914
|
+
|
|
50915
|
+
}
|
|
50916
|
+
|
|
50917
|
+
}
|
|
50918
|
+
|
|
50919
|
+
// generate result BufferGeometry
|
|
50920
|
+
const result = geometry.clone();
|
|
50921
|
+
for ( const name in geometry.attributes ) {
|
|
50922
|
+
|
|
50923
|
+
const tmpAttribute = tmpAttributes[ name ];
|
|
50924
|
+
|
|
50925
|
+
result.setAttribute( name, new tmpAttribute.constructor(
|
|
50926
|
+
tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ),
|
|
50927
|
+
tmpAttribute.itemSize,
|
|
50928
|
+
tmpAttribute.normalized,
|
|
50929
|
+
) );
|
|
50930
|
+
|
|
50931
|
+
if ( ! ( name in tmpMorphAttributes ) ) continue;
|
|
50932
|
+
|
|
50933
|
+
for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) {
|
|
50934
|
+
|
|
50935
|
+
const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ];
|
|
50936
|
+
|
|
50937
|
+
result.morphAttributes[ name ][ j ] = new tmpMorphAttribute.constructor(
|
|
50938
|
+
tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ),
|
|
50939
|
+
tmpMorphAttribute.itemSize,
|
|
50940
|
+
tmpMorphAttribute.normalized,
|
|
50941
|
+
);
|
|
50942
|
+
|
|
50943
|
+
}
|
|
50944
|
+
|
|
50945
|
+
}
|
|
50946
|
+
|
|
50947
|
+
// indices
|
|
50948
|
+
|
|
50949
|
+
result.setIndex( newIndices );
|
|
50950
|
+
|
|
50951
|
+
return result;
|
|
50952
|
+
|
|
50953
|
+
}
|
|
50954
|
+
|
|
50306
50955
|
/**
|
|
50307
50956
|
* @param {BufferGeometry} geometry
|
|
50308
50957
|
* @param {number} drawMode
|
|
@@ -58680,185 +59329,2351 @@ function onMouseDown( event ) {
|
|
|
58680
59329
|
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
58681
59330
|
this._movePrev.copy( this._moveCurr );
|
|
58682
59331
|
|
|
58683
|
-
} else if ( state === _STATE.ZOOM && ! this.noZoom ) {
|
|
59332
|
+
} else if ( state === _STATE.ZOOM && ! this.noZoom ) {
|
|
59333
|
+
|
|
59334
|
+
this._zoomStart.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
|
|
59335
|
+
this._zoomEnd.copy( this._zoomStart );
|
|
59336
|
+
|
|
59337
|
+
} else if ( state === _STATE.PAN && ! this.noPan ) {
|
|
59338
|
+
|
|
59339
|
+
this._panStart.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
|
|
59340
|
+
this._panEnd.copy( this._panStart );
|
|
59341
|
+
|
|
59342
|
+
}
|
|
59343
|
+
|
|
59344
|
+
this.dispatchEvent( _startEvent );
|
|
59345
|
+
|
|
59346
|
+
}
|
|
59347
|
+
|
|
59348
|
+
function onMouseMove( event ) {
|
|
59349
|
+
|
|
59350
|
+
const state = ( this.keyState !== _STATE.NONE ) ? this.keyState : this.state;
|
|
59351
|
+
|
|
59352
|
+
if ( state === _STATE.ROTATE && ! this.noRotate ) {
|
|
59353
|
+
|
|
59354
|
+
this._movePrev.copy( this._moveCurr );
|
|
59355
|
+
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
59356
|
+
|
|
59357
|
+
} else if ( state === _STATE.ZOOM && ! this.noZoom ) {
|
|
59358
|
+
|
|
59359
|
+
this._zoomEnd.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
|
|
59360
|
+
|
|
59361
|
+
} else if ( state === _STATE.PAN && ! this.noPan ) {
|
|
59362
|
+
|
|
59363
|
+
this._panEnd.copy( this._getMouseOnScreen( event.pageX, event.pageY ) );
|
|
59364
|
+
|
|
59365
|
+
}
|
|
59366
|
+
|
|
59367
|
+
}
|
|
59368
|
+
|
|
59369
|
+
function onMouseUp() {
|
|
59370
|
+
|
|
59371
|
+
this.state = _STATE.NONE;
|
|
59372
|
+
|
|
59373
|
+
this.dispatchEvent( _endEvent );
|
|
59374
|
+
|
|
59375
|
+
}
|
|
59376
|
+
|
|
59377
|
+
function onMouseWheel( event ) {
|
|
59378
|
+
|
|
59379
|
+
if ( this.enabled === false ) return;
|
|
59380
|
+
|
|
59381
|
+
if ( this.noZoom === true ) return;
|
|
59382
|
+
|
|
59383
|
+
event.preventDefault();
|
|
59384
|
+
|
|
59385
|
+
switch ( event.deltaMode ) {
|
|
59386
|
+
|
|
59387
|
+
case 2:
|
|
59388
|
+
// Zoom in pages
|
|
59389
|
+
this._zoomStart.y -= event.deltaY * 0.025;
|
|
59390
|
+
break;
|
|
59391
|
+
|
|
59392
|
+
case 1:
|
|
59393
|
+
// Zoom in lines
|
|
59394
|
+
this._zoomStart.y -= event.deltaY * 0.01;
|
|
59395
|
+
break;
|
|
59396
|
+
|
|
59397
|
+
default:
|
|
59398
|
+
// undefined, 0, assume pixels
|
|
59399
|
+
this._zoomStart.y -= event.deltaY * 0.00025;
|
|
59400
|
+
break;
|
|
59401
|
+
|
|
59402
|
+
}
|
|
59403
|
+
|
|
59404
|
+
this.dispatchEvent( _startEvent );
|
|
59405
|
+
this.dispatchEvent( _endEvent );
|
|
59406
|
+
|
|
59407
|
+
}
|
|
59408
|
+
|
|
59409
|
+
function onContextMenu( event ) {
|
|
59410
|
+
|
|
59411
|
+
if ( this.enabled === false ) return;
|
|
59412
|
+
|
|
59413
|
+
event.preventDefault();
|
|
59414
|
+
|
|
59415
|
+
}
|
|
59416
|
+
|
|
59417
|
+
function onTouchStart( event ) {
|
|
59418
|
+
|
|
59419
|
+
this._trackPointer( event );
|
|
59420
|
+
|
|
59421
|
+
switch ( this._pointers.length ) {
|
|
59422
|
+
|
|
59423
|
+
case 1:
|
|
59424
|
+
this.state = _STATE.TOUCH_ROTATE;
|
|
59425
|
+
this._moveCurr.copy( this._getMouseOnCircle( this._pointers[ 0 ].pageX, this._pointers[ 0 ].pageY ) );
|
|
59426
|
+
this._movePrev.copy( this._moveCurr );
|
|
59427
|
+
break;
|
|
59428
|
+
|
|
59429
|
+
default: // 2 or more
|
|
59430
|
+
this.state = _STATE.TOUCH_ZOOM_PAN;
|
|
59431
|
+
const dx = this._pointers[ 0 ].pageX - this._pointers[ 1 ].pageX;
|
|
59432
|
+
const dy = this._pointers[ 0 ].pageY - this._pointers[ 1 ].pageY;
|
|
59433
|
+
this._touchZoomDistanceEnd = this._touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
|
|
59434
|
+
|
|
59435
|
+
const x = ( this._pointers[ 0 ].pageX + this._pointers[ 1 ].pageX ) / 2;
|
|
59436
|
+
const y = ( this._pointers[ 0 ].pageY + this._pointers[ 1 ].pageY ) / 2;
|
|
59437
|
+
this._panStart.copy( this._getMouseOnScreen( x, y ) );
|
|
59438
|
+
this._panEnd.copy( this._panStart );
|
|
59439
|
+
break;
|
|
59440
|
+
|
|
59441
|
+
}
|
|
59442
|
+
|
|
59443
|
+
this.dispatchEvent( _startEvent );
|
|
59444
|
+
|
|
59445
|
+
}
|
|
59446
|
+
|
|
59447
|
+
function onTouchMove( event ) {
|
|
59448
|
+
|
|
59449
|
+
this._trackPointer( event );
|
|
59450
|
+
|
|
59451
|
+
switch ( this._pointers.length ) {
|
|
59452
|
+
|
|
59453
|
+
case 1:
|
|
59454
|
+
this._movePrev.copy( this._moveCurr );
|
|
59455
|
+
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
59456
|
+
break;
|
|
59457
|
+
|
|
59458
|
+
default: // 2 or more
|
|
59459
|
+
|
|
59460
|
+
const position = this._getSecondPointerPosition( event );
|
|
59461
|
+
|
|
59462
|
+
const dx = event.pageX - position.x;
|
|
59463
|
+
const dy = event.pageY - position.y;
|
|
59464
|
+
this._touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
|
|
59465
|
+
|
|
59466
|
+
const x = ( event.pageX + position.x ) / 2;
|
|
59467
|
+
const y = ( event.pageY + position.y ) / 2;
|
|
59468
|
+
this._panEnd.copy( this._getMouseOnScreen( x, y ) );
|
|
59469
|
+
break;
|
|
59470
|
+
|
|
59471
|
+
}
|
|
59472
|
+
|
|
59473
|
+
}
|
|
59474
|
+
|
|
59475
|
+
function onTouchEnd( event ) {
|
|
59476
|
+
|
|
59477
|
+
switch ( this._pointers.length ) {
|
|
59478
|
+
|
|
59479
|
+
case 0:
|
|
59480
|
+
this.state = _STATE.NONE;
|
|
59481
|
+
break;
|
|
59482
|
+
|
|
59483
|
+
case 1:
|
|
59484
|
+
this.state = _STATE.TOUCH_ROTATE;
|
|
59485
|
+
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
59486
|
+
this._movePrev.copy( this._moveCurr );
|
|
59487
|
+
break;
|
|
59488
|
+
|
|
59489
|
+
case 2:
|
|
59490
|
+
this.state = _STATE.TOUCH_ZOOM_PAN;
|
|
59491
|
+
|
|
59492
|
+
for ( let i = 0; i < this._pointers.length; i ++ ) {
|
|
59493
|
+
|
|
59494
|
+
if ( this._pointers[ i ].pointerId !== event.pointerId ) {
|
|
59495
|
+
|
|
59496
|
+
const position = this._pointerPositions[ this._pointers[ i ].pointerId ];
|
|
59497
|
+
this._moveCurr.copy( this._getMouseOnCircle( position.x, position.y ) );
|
|
59498
|
+
this._movePrev.copy( this._moveCurr );
|
|
59499
|
+
break;
|
|
59500
|
+
|
|
59501
|
+
}
|
|
59502
|
+
|
|
59503
|
+
}
|
|
59504
|
+
|
|
59505
|
+
break;
|
|
59506
|
+
|
|
59507
|
+
}
|
|
59508
|
+
|
|
59509
|
+
this.dispatchEvent( _endEvent );
|
|
59510
|
+
|
|
59511
|
+
}
|
|
59512
|
+
|
|
59513
|
+
const _inv = new Matrix4();
|
|
59514
|
+
/** 世界系命中点 → local 顶点(位置 worldToLocal,法线用逆变换)。 */
|
|
59515
|
+
function worldHitToLocalVertex(h, mesh) {
|
|
59516
|
+
const lp = mesh.worldToLocal(h.point.clone());
|
|
59517
|
+
_inv.copy(mesh.matrixWorld).invert();
|
|
59518
|
+
const ln = h.normal.clone().transformDirection(_inv).normalize();
|
|
59519
|
+
return {
|
|
59520
|
+
x: lp.x,
|
|
59521
|
+
y: lp.y,
|
|
59522
|
+
z: lp.z,
|
|
59523
|
+
nx: ln.x,
|
|
59524
|
+
ny: ln.y,
|
|
59525
|
+
nz: ln.z,
|
|
59526
|
+
faceIndex: h.faceIndex,
|
|
59527
|
+
};
|
|
59528
|
+
}
|
|
59529
|
+
const _nm = new Matrix3();
|
|
59530
|
+
/** local 顶点 → world 位置 + world 法线(供渲染)。 */
|
|
59531
|
+
function localVertexToWorld(v, mesh) {
|
|
59532
|
+
const p = new Vector3(v.x, v.y, v.z).applyMatrix4(mesh.matrixWorld);
|
|
59533
|
+
_nm.getNormalMatrix(mesh.matrixWorld);
|
|
59534
|
+
const n = new Vector3(v.nx, v.ny, v.nz).applyMatrix3(_nm).normalize();
|
|
59535
|
+
return { p, n };
|
|
59536
|
+
}
|
|
59537
|
+
|
|
59538
|
+
const ray = new Raycaster();
|
|
59539
|
+
const ndc = new Vector2();
|
|
59540
|
+
/**
|
|
59541
|
+
* 把屏幕坐标(clientX/Y)投射到 mesh 表面,返回命中点、world-space 法线与 faceIndex。
|
|
59542
|
+
* 未命中返回 null。本地实现,不依赖 Copper3D 未导出的 raycast 内部函数。
|
|
59543
|
+
*/
|
|
59544
|
+
function raycastSurface(camera, container, mesh, clientX, clientY) {
|
|
59545
|
+
var _a;
|
|
59546
|
+
const rect = container.getBoundingClientRect();
|
|
59547
|
+
ndc.x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
|
59548
|
+
ndc.y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
|
59549
|
+
ray.setFromCamera(ndc, camera);
|
|
59550
|
+
const hits = ray.intersectObject(mesh, false);
|
|
59551
|
+
if (hits.length === 0)
|
|
59552
|
+
return null;
|
|
59553
|
+
const h = hits[0];
|
|
59554
|
+
const point = h.point.clone();
|
|
59555
|
+
const normal = new Vector3(0, 0, 1);
|
|
59556
|
+
if (h.face) {
|
|
59557
|
+
normal.copy(h.face.normal);
|
|
59558
|
+
const nm = new Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
59559
|
+
normal.applyMatrix3(nm).normalize();
|
|
59560
|
+
}
|
|
59561
|
+
return { point, normal, faceIndex: (_a = h.faceIndex) !== null && _a !== void 0 ? _a : -1 };
|
|
59562
|
+
}
|
|
59563
|
+
|
|
59564
|
+
/**
|
|
59565
|
+
* 在 local 顶点处生成一个标记小球(fiducial)。local→world 后沿 world 法线略外移,
|
|
59566
|
+
* 避免一半埋进表面。radius 由调用方按模型 bbox 缩放传入。
|
|
59567
|
+
*/
|
|
59568
|
+
function makePointMarker(v, mesh, color, radius) {
|
|
59569
|
+
const geo = new SphereGeometry(radius, 16, 16);
|
|
59570
|
+
const mat = new MeshBasicMaterial({ color });
|
|
59571
|
+
const m = new Mesh(geo, mat);
|
|
59572
|
+
const { p, n } = localVertexToWorld(v, mesh);
|
|
59573
|
+
m.position.copy(p.addScaledVector(n, radius * 0.5));
|
|
59574
|
+
m.renderOrder = 999;
|
|
59575
|
+
return m;
|
|
59576
|
+
}
|
|
59577
|
+
|
|
59578
|
+
/**
|
|
59579
|
+
* 模式 A(自由手绘)一笔的状态机。
|
|
59580
|
+
* pointermove 时不断 addSample;与上一采样点世界距离 < minGap 的样本丢弃,避免过密。
|
|
59581
|
+
* 顶点以 local 存(由 mesh 把世界命中点转 local)。
|
|
59582
|
+
*/
|
|
59583
|
+
class StrokeContour {
|
|
59584
|
+
constructor(minGap, mesh) {
|
|
59585
|
+
this.minGap = minGap;
|
|
59586
|
+
this.mesh = mesh;
|
|
59587
|
+
this.verts = [];
|
|
59588
|
+
this.last = new Vector3();
|
|
59589
|
+
this.has = false;
|
|
59590
|
+
}
|
|
59591
|
+
begin() {
|
|
59592
|
+
this.verts = [];
|
|
59593
|
+
this.has = false;
|
|
59594
|
+
}
|
|
59595
|
+
addSample(hit) {
|
|
59596
|
+
// gap 判定用世界距离(屏幕笔触在世界系采样)
|
|
59597
|
+
if (this.has && hit.point.distanceTo(this.last) < this.minGap)
|
|
59598
|
+
return;
|
|
59599
|
+
this.verts.push(worldHitToLocalVertex(hit, this.mesh));
|
|
59600
|
+
this.last.copy(hit.point);
|
|
59601
|
+
this.has = true;
|
|
59602
|
+
}
|
|
59603
|
+
get vertices() {
|
|
59604
|
+
return this.verts;
|
|
59605
|
+
}
|
|
59606
|
+
end() {
|
|
59607
|
+
return this.verts;
|
|
59608
|
+
}
|
|
59609
|
+
}
|
|
59610
|
+
|
|
59611
|
+
const _box$1 = new Box3();
|
|
59612
|
+
const _vector = new Vector3();
|
|
59613
|
+
|
|
59614
|
+
class LineSegmentsGeometry extends InstancedBufferGeometry {
|
|
59615
|
+
|
|
59616
|
+
constructor() {
|
|
59617
|
+
|
|
59618
|
+
super();
|
|
59619
|
+
|
|
59620
|
+
this.isLineSegmentsGeometry = true;
|
|
59621
|
+
|
|
59622
|
+
this.type = 'LineSegmentsGeometry';
|
|
59623
|
+
|
|
59624
|
+
const positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ];
|
|
59625
|
+
const uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ];
|
|
59626
|
+
const index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ];
|
|
59627
|
+
|
|
59628
|
+
this.setIndex( index );
|
|
59629
|
+
this.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
|
|
59630
|
+
this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
|
|
59631
|
+
|
|
59632
|
+
}
|
|
59633
|
+
|
|
59634
|
+
applyMatrix4( matrix ) {
|
|
59635
|
+
|
|
59636
|
+
const start = this.attributes.instanceStart;
|
|
59637
|
+
const end = this.attributes.instanceEnd;
|
|
59638
|
+
|
|
59639
|
+
if ( start !== undefined ) {
|
|
59640
|
+
|
|
59641
|
+
start.applyMatrix4( matrix );
|
|
59642
|
+
|
|
59643
|
+
end.applyMatrix4( matrix );
|
|
59644
|
+
|
|
59645
|
+
start.needsUpdate = true;
|
|
59646
|
+
|
|
59647
|
+
}
|
|
59648
|
+
|
|
59649
|
+
if ( this.boundingBox !== null ) {
|
|
59650
|
+
|
|
59651
|
+
this.computeBoundingBox();
|
|
59652
|
+
|
|
59653
|
+
}
|
|
59654
|
+
|
|
59655
|
+
if ( this.boundingSphere !== null ) {
|
|
59656
|
+
|
|
59657
|
+
this.computeBoundingSphere();
|
|
59658
|
+
|
|
59659
|
+
}
|
|
59660
|
+
|
|
59661
|
+
return this;
|
|
59662
|
+
|
|
59663
|
+
}
|
|
59664
|
+
|
|
59665
|
+
setPositions( array ) {
|
|
59666
|
+
|
|
59667
|
+
let lineSegments;
|
|
59668
|
+
|
|
59669
|
+
if ( array instanceof Float32Array ) {
|
|
59670
|
+
|
|
59671
|
+
lineSegments = array;
|
|
59672
|
+
|
|
59673
|
+
} else if ( Array.isArray( array ) ) {
|
|
59674
|
+
|
|
59675
|
+
lineSegments = new Float32Array( array );
|
|
59676
|
+
|
|
59677
|
+
}
|
|
59678
|
+
|
|
59679
|
+
const instanceBuffer = new InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz
|
|
59680
|
+
|
|
59681
|
+
this.setAttribute( 'instanceStart', new InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz
|
|
59682
|
+
this.setAttribute( 'instanceEnd', new InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz
|
|
59683
|
+
|
|
59684
|
+
this.instanceCount = this.attributes.instanceStart.count;
|
|
59685
|
+
|
|
59686
|
+
//
|
|
59687
|
+
|
|
59688
|
+
this.computeBoundingBox();
|
|
59689
|
+
this.computeBoundingSphere();
|
|
59690
|
+
|
|
59691
|
+
return this;
|
|
59692
|
+
|
|
59693
|
+
}
|
|
59694
|
+
|
|
59695
|
+
setColors( array ) {
|
|
59696
|
+
|
|
59697
|
+
let colors;
|
|
59698
|
+
|
|
59699
|
+
if ( array instanceof Float32Array ) {
|
|
59700
|
+
|
|
59701
|
+
colors = array;
|
|
59702
|
+
|
|
59703
|
+
} else if ( Array.isArray( array ) ) {
|
|
59704
|
+
|
|
59705
|
+
colors = new Float32Array( array );
|
|
59706
|
+
|
|
59707
|
+
}
|
|
59708
|
+
|
|
59709
|
+
const instanceColorBuffer = new InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb
|
|
59710
|
+
|
|
59711
|
+
this.setAttribute( 'instanceColorStart', new InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb
|
|
59712
|
+
this.setAttribute( 'instanceColorEnd', new InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb
|
|
59713
|
+
|
|
59714
|
+
return this;
|
|
59715
|
+
|
|
59716
|
+
}
|
|
59717
|
+
|
|
59718
|
+
fromWireframeGeometry( geometry ) {
|
|
59719
|
+
|
|
59720
|
+
this.setPositions( geometry.attributes.position.array );
|
|
59721
|
+
|
|
59722
|
+
return this;
|
|
59723
|
+
|
|
59724
|
+
}
|
|
59725
|
+
|
|
59726
|
+
fromEdgesGeometry( geometry ) {
|
|
59727
|
+
|
|
59728
|
+
this.setPositions( geometry.attributes.position.array );
|
|
59729
|
+
|
|
59730
|
+
return this;
|
|
59731
|
+
|
|
59732
|
+
}
|
|
59733
|
+
|
|
59734
|
+
fromMesh( mesh ) {
|
|
59735
|
+
|
|
59736
|
+
this.fromWireframeGeometry( new WireframeGeometry( mesh.geometry ) );
|
|
59737
|
+
|
|
59738
|
+
// set colors, maybe
|
|
59739
|
+
|
|
59740
|
+
return this;
|
|
59741
|
+
|
|
59742
|
+
}
|
|
59743
|
+
|
|
59744
|
+
fromLineSegments( lineSegments ) {
|
|
59745
|
+
|
|
59746
|
+
const geometry = lineSegments.geometry;
|
|
59747
|
+
|
|
59748
|
+
this.setPositions( geometry.attributes.position.array ); // assumes non-indexed
|
|
59749
|
+
|
|
59750
|
+
// set colors, maybe
|
|
59751
|
+
|
|
59752
|
+
return this;
|
|
59753
|
+
|
|
59754
|
+
}
|
|
59755
|
+
|
|
59756
|
+
computeBoundingBox() {
|
|
59757
|
+
|
|
59758
|
+
if ( this.boundingBox === null ) {
|
|
59759
|
+
|
|
59760
|
+
this.boundingBox = new Box3();
|
|
59761
|
+
|
|
59762
|
+
}
|
|
59763
|
+
|
|
59764
|
+
const start = this.attributes.instanceStart;
|
|
59765
|
+
const end = this.attributes.instanceEnd;
|
|
59766
|
+
|
|
59767
|
+
if ( start !== undefined && end !== undefined ) {
|
|
59768
|
+
|
|
59769
|
+
this.boundingBox.setFromBufferAttribute( start );
|
|
59770
|
+
|
|
59771
|
+
_box$1.setFromBufferAttribute( end );
|
|
59772
|
+
|
|
59773
|
+
this.boundingBox.union( _box$1 );
|
|
59774
|
+
|
|
59775
|
+
}
|
|
59776
|
+
|
|
59777
|
+
}
|
|
59778
|
+
|
|
59779
|
+
computeBoundingSphere() {
|
|
59780
|
+
|
|
59781
|
+
if ( this.boundingSphere === null ) {
|
|
59782
|
+
|
|
59783
|
+
this.boundingSphere = new Sphere();
|
|
59784
|
+
|
|
59785
|
+
}
|
|
59786
|
+
|
|
59787
|
+
if ( this.boundingBox === null ) {
|
|
59788
|
+
|
|
59789
|
+
this.computeBoundingBox();
|
|
59790
|
+
|
|
59791
|
+
}
|
|
59792
|
+
|
|
59793
|
+
const start = this.attributes.instanceStart;
|
|
59794
|
+
const end = this.attributes.instanceEnd;
|
|
59795
|
+
|
|
59796
|
+
if ( start !== undefined && end !== undefined ) {
|
|
59797
|
+
|
|
59798
|
+
const center = this.boundingSphere.center;
|
|
59799
|
+
|
|
59800
|
+
this.boundingBox.getCenter( center );
|
|
59801
|
+
|
|
59802
|
+
let maxRadiusSq = 0;
|
|
59803
|
+
|
|
59804
|
+
for ( let i = 0, il = start.count; i < il; i ++ ) {
|
|
59805
|
+
|
|
59806
|
+
_vector.fromBufferAttribute( start, i );
|
|
59807
|
+
maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) );
|
|
59808
|
+
|
|
59809
|
+
_vector.fromBufferAttribute( end, i );
|
|
59810
|
+
maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) );
|
|
59811
|
+
|
|
59812
|
+
}
|
|
59813
|
+
|
|
59814
|
+
this.boundingSphere.radius = Math.sqrt( maxRadiusSq );
|
|
59815
|
+
|
|
59816
|
+
if ( isNaN( this.boundingSphere.radius ) ) {
|
|
59817
|
+
|
|
59818
|
+
console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this );
|
|
59819
|
+
|
|
59820
|
+
}
|
|
59821
|
+
|
|
59822
|
+
}
|
|
59823
|
+
|
|
59824
|
+
}
|
|
59825
|
+
|
|
59826
|
+
toJSON() {
|
|
59827
|
+
|
|
59828
|
+
// todo
|
|
59829
|
+
|
|
59830
|
+
}
|
|
59831
|
+
|
|
59832
|
+
applyMatrix( matrix ) {
|
|
59833
|
+
|
|
59834
|
+
console.warn( 'THREE.LineSegmentsGeometry: applyMatrix() has been renamed to applyMatrix4().' );
|
|
59835
|
+
|
|
59836
|
+
return this.applyMatrix4( matrix );
|
|
59837
|
+
|
|
59838
|
+
}
|
|
59839
|
+
|
|
59840
|
+
}
|
|
59841
|
+
|
|
59842
|
+
UniformsLib.line = {
|
|
59843
|
+
|
|
59844
|
+
worldUnits: { value: 1 },
|
|
59845
|
+
linewidth: { value: 1 },
|
|
59846
|
+
resolution: { value: new Vector2( 1, 1 ) },
|
|
59847
|
+
dashOffset: { value: 0 },
|
|
59848
|
+
dashScale: { value: 1 },
|
|
59849
|
+
dashSize: { value: 1 },
|
|
59850
|
+
gapSize: { value: 1 } // todo FIX - maybe change to totalSize
|
|
59851
|
+
|
|
59852
|
+
};
|
|
59853
|
+
|
|
59854
|
+
ShaderLib[ 'line' ] = {
|
|
59855
|
+
|
|
59856
|
+
uniforms: UniformsUtils.merge( [
|
|
59857
|
+
UniformsLib.common,
|
|
59858
|
+
UniformsLib.fog,
|
|
59859
|
+
UniformsLib.line
|
|
59860
|
+
] ),
|
|
59861
|
+
|
|
59862
|
+
vertexShader:
|
|
59863
|
+
/* glsl */`
|
|
59864
|
+
#include <common>
|
|
59865
|
+
#include <color_pars_vertex>
|
|
59866
|
+
#include <fog_pars_vertex>
|
|
59867
|
+
#include <logdepthbuf_pars_vertex>
|
|
59868
|
+
#include <clipping_planes_pars_vertex>
|
|
59869
|
+
|
|
59870
|
+
uniform float linewidth;
|
|
59871
|
+
uniform vec2 resolution;
|
|
59872
|
+
|
|
59873
|
+
attribute vec3 instanceStart;
|
|
59874
|
+
attribute vec3 instanceEnd;
|
|
59875
|
+
|
|
59876
|
+
attribute vec3 instanceColorStart;
|
|
59877
|
+
attribute vec3 instanceColorEnd;
|
|
59878
|
+
|
|
59879
|
+
#ifdef WORLD_UNITS
|
|
59880
|
+
|
|
59881
|
+
varying vec4 worldPos;
|
|
59882
|
+
varying vec3 worldStart;
|
|
59883
|
+
varying vec3 worldEnd;
|
|
59884
|
+
|
|
59885
|
+
#ifdef USE_DASH
|
|
59886
|
+
|
|
59887
|
+
varying vec2 vUv;
|
|
59888
|
+
|
|
59889
|
+
#endif
|
|
59890
|
+
|
|
59891
|
+
#else
|
|
59892
|
+
|
|
59893
|
+
varying vec2 vUv;
|
|
59894
|
+
|
|
59895
|
+
#endif
|
|
59896
|
+
|
|
59897
|
+
#ifdef USE_DASH
|
|
59898
|
+
|
|
59899
|
+
uniform float dashScale;
|
|
59900
|
+
attribute float instanceDistanceStart;
|
|
59901
|
+
attribute float instanceDistanceEnd;
|
|
59902
|
+
varying float vLineDistance;
|
|
59903
|
+
|
|
59904
|
+
#endif
|
|
59905
|
+
|
|
59906
|
+
void trimSegment( const in vec4 start, inout vec4 end ) {
|
|
59907
|
+
|
|
59908
|
+
// trim end segment so it terminates between the camera plane and the near plane
|
|
59909
|
+
|
|
59910
|
+
// conservative estimate of the near plane
|
|
59911
|
+
float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
|
|
59912
|
+
float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
|
|
59913
|
+
float nearEstimate = - 0.5 * b / a;
|
|
59914
|
+
|
|
59915
|
+
float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );
|
|
59916
|
+
|
|
59917
|
+
end.xyz = mix( start.xyz, end.xyz, alpha );
|
|
59918
|
+
|
|
59919
|
+
}
|
|
59920
|
+
|
|
59921
|
+
void main() {
|
|
59922
|
+
|
|
59923
|
+
#ifdef USE_COLOR
|
|
59924
|
+
|
|
59925
|
+
vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;
|
|
59926
|
+
|
|
59927
|
+
#endif
|
|
59928
|
+
|
|
59929
|
+
#ifdef USE_DASH
|
|
59930
|
+
|
|
59931
|
+
vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;
|
|
59932
|
+
vUv = uv;
|
|
59933
|
+
|
|
59934
|
+
#endif
|
|
59935
|
+
|
|
59936
|
+
float aspect = resolution.x / resolution.y;
|
|
59937
|
+
|
|
59938
|
+
// camera space
|
|
59939
|
+
vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
|
|
59940
|
+
vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );
|
|
59941
|
+
|
|
59942
|
+
#ifdef WORLD_UNITS
|
|
59943
|
+
|
|
59944
|
+
worldStart = start.xyz;
|
|
59945
|
+
worldEnd = end.xyz;
|
|
59946
|
+
|
|
59947
|
+
#else
|
|
59948
|
+
|
|
59949
|
+
vUv = uv;
|
|
59950
|
+
|
|
59951
|
+
#endif
|
|
59952
|
+
|
|
59953
|
+
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
|
|
59954
|
+
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
|
|
59955
|
+
// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
|
|
59956
|
+
// perhaps there is a more elegant solution -- WestLangley
|
|
59957
|
+
|
|
59958
|
+
bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column
|
|
59959
|
+
|
|
59960
|
+
if ( perspective ) {
|
|
59961
|
+
|
|
59962
|
+
if ( start.z < 0.0 && end.z >= 0.0 ) {
|
|
59963
|
+
|
|
59964
|
+
trimSegment( start, end );
|
|
59965
|
+
|
|
59966
|
+
} else if ( end.z < 0.0 && start.z >= 0.0 ) {
|
|
59967
|
+
|
|
59968
|
+
trimSegment( end, start );
|
|
59969
|
+
|
|
59970
|
+
}
|
|
59971
|
+
|
|
59972
|
+
}
|
|
59973
|
+
|
|
59974
|
+
// clip space
|
|
59975
|
+
vec4 clipStart = projectionMatrix * start;
|
|
59976
|
+
vec4 clipEnd = projectionMatrix * end;
|
|
59977
|
+
|
|
59978
|
+
// ndc space
|
|
59979
|
+
vec3 ndcStart = clipStart.xyz / clipStart.w;
|
|
59980
|
+
vec3 ndcEnd = clipEnd.xyz / clipEnd.w;
|
|
59981
|
+
|
|
59982
|
+
// direction
|
|
59983
|
+
vec2 dir = ndcEnd.xy - ndcStart.xy;
|
|
59984
|
+
|
|
59985
|
+
// account for clip-space aspect ratio
|
|
59986
|
+
dir.x *= aspect;
|
|
59987
|
+
dir = normalize( dir );
|
|
59988
|
+
|
|
59989
|
+
#ifdef WORLD_UNITS
|
|
59990
|
+
|
|
59991
|
+
vec3 worldDir = normalize( end.xyz - start.xyz );
|
|
59992
|
+
vec3 tmpFwd = normalize( mix( start.xyz, end.xyz, 0.5 ) );
|
|
59993
|
+
vec3 worldUp = normalize( cross( worldDir, tmpFwd ) );
|
|
59994
|
+
vec3 worldFwd = cross( worldDir, worldUp );
|
|
59995
|
+
worldPos = position.y < 0.5 ? start: end;
|
|
59996
|
+
|
|
59997
|
+
// height offset
|
|
59998
|
+
float hw = linewidth * 0.5;
|
|
59999
|
+
worldPos.xyz += position.x < 0.0 ? hw * worldUp : - hw * worldUp;
|
|
60000
|
+
|
|
60001
|
+
// don't extend the line if we're rendering dashes because we
|
|
60002
|
+
// won't be rendering the endcaps
|
|
60003
|
+
#ifndef USE_DASH
|
|
60004
|
+
|
|
60005
|
+
// cap extension
|
|
60006
|
+
worldPos.xyz += position.y < 0.5 ? - hw * worldDir : hw * worldDir;
|
|
60007
|
+
|
|
60008
|
+
// add width to the box
|
|
60009
|
+
worldPos.xyz += worldFwd * hw;
|
|
60010
|
+
|
|
60011
|
+
// endcaps
|
|
60012
|
+
if ( position.y > 1.0 || position.y < 0.0 ) {
|
|
60013
|
+
|
|
60014
|
+
worldPos.xyz -= worldFwd * 2.0 * hw;
|
|
60015
|
+
|
|
60016
|
+
}
|
|
60017
|
+
|
|
60018
|
+
#endif
|
|
60019
|
+
|
|
60020
|
+
// project the worldpos
|
|
60021
|
+
vec4 clip = projectionMatrix * worldPos;
|
|
60022
|
+
|
|
60023
|
+
// shift the depth of the projected points so the line
|
|
60024
|
+
// segments overlap neatly
|
|
60025
|
+
vec3 clipPose = ( position.y < 0.5 ) ? ndcStart : ndcEnd;
|
|
60026
|
+
clip.z = clipPose.z * clip.w;
|
|
60027
|
+
|
|
60028
|
+
#else
|
|
60029
|
+
|
|
60030
|
+
vec2 offset = vec2( dir.y, - dir.x );
|
|
60031
|
+
// undo aspect ratio adjustment
|
|
60032
|
+
dir.x /= aspect;
|
|
60033
|
+
offset.x /= aspect;
|
|
60034
|
+
|
|
60035
|
+
// sign flip
|
|
60036
|
+
if ( position.x < 0.0 ) offset *= - 1.0;
|
|
60037
|
+
|
|
60038
|
+
// endcaps
|
|
60039
|
+
if ( position.y < 0.0 ) {
|
|
60040
|
+
|
|
60041
|
+
offset += - dir;
|
|
60042
|
+
|
|
60043
|
+
} else if ( position.y > 1.0 ) {
|
|
60044
|
+
|
|
60045
|
+
offset += dir;
|
|
60046
|
+
|
|
60047
|
+
}
|
|
60048
|
+
|
|
60049
|
+
// adjust for linewidth
|
|
60050
|
+
offset *= linewidth;
|
|
60051
|
+
|
|
60052
|
+
// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
|
|
60053
|
+
offset /= resolution.y;
|
|
60054
|
+
|
|
60055
|
+
// select end
|
|
60056
|
+
vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;
|
|
60057
|
+
|
|
60058
|
+
// back to clip space
|
|
60059
|
+
offset *= clip.w;
|
|
60060
|
+
|
|
60061
|
+
clip.xy += offset;
|
|
60062
|
+
|
|
60063
|
+
#endif
|
|
60064
|
+
|
|
60065
|
+
gl_Position = clip;
|
|
60066
|
+
|
|
60067
|
+
vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation
|
|
60068
|
+
|
|
60069
|
+
#include <logdepthbuf_vertex>
|
|
60070
|
+
#include <clipping_planes_vertex>
|
|
60071
|
+
#include <fog_vertex>
|
|
60072
|
+
|
|
60073
|
+
}
|
|
60074
|
+
`,
|
|
60075
|
+
|
|
60076
|
+
fragmentShader:
|
|
60077
|
+
/* glsl */`
|
|
60078
|
+
uniform vec3 diffuse;
|
|
60079
|
+
uniform float opacity;
|
|
60080
|
+
uniform float linewidth;
|
|
60081
|
+
|
|
60082
|
+
#ifdef USE_DASH
|
|
60083
|
+
|
|
60084
|
+
uniform float dashOffset;
|
|
60085
|
+
uniform float dashSize;
|
|
60086
|
+
uniform float gapSize;
|
|
60087
|
+
|
|
60088
|
+
#endif
|
|
60089
|
+
|
|
60090
|
+
varying float vLineDistance;
|
|
60091
|
+
|
|
60092
|
+
#ifdef WORLD_UNITS
|
|
60093
|
+
|
|
60094
|
+
varying vec4 worldPos;
|
|
60095
|
+
varying vec3 worldStart;
|
|
60096
|
+
varying vec3 worldEnd;
|
|
60097
|
+
|
|
60098
|
+
#ifdef USE_DASH
|
|
60099
|
+
|
|
60100
|
+
varying vec2 vUv;
|
|
60101
|
+
|
|
60102
|
+
#endif
|
|
60103
|
+
|
|
60104
|
+
#else
|
|
60105
|
+
|
|
60106
|
+
varying vec2 vUv;
|
|
60107
|
+
|
|
60108
|
+
#endif
|
|
60109
|
+
|
|
60110
|
+
#include <common>
|
|
60111
|
+
#include <color_pars_fragment>
|
|
60112
|
+
#include <fog_pars_fragment>
|
|
60113
|
+
#include <logdepthbuf_pars_fragment>
|
|
60114
|
+
#include <clipping_planes_pars_fragment>
|
|
60115
|
+
|
|
60116
|
+
vec2 closestLineToLine(vec3 p1, vec3 p2, vec3 p3, vec3 p4) {
|
|
60117
|
+
|
|
60118
|
+
float mua;
|
|
60119
|
+
float mub;
|
|
60120
|
+
|
|
60121
|
+
vec3 p13 = p1 - p3;
|
|
60122
|
+
vec3 p43 = p4 - p3;
|
|
60123
|
+
|
|
60124
|
+
vec3 p21 = p2 - p1;
|
|
60125
|
+
|
|
60126
|
+
float d1343 = dot( p13, p43 );
|
|
60127
|
+
float d4321 = dot( p43, p21 );
|
|
60128
|
+
float d1321 = dot( p13, p21 );
|
|
60129
|
+
float d4343 = dot( p43, p43 );
|
|
60130
|
+
float d2121 = dot( p21, p21 );
|
|
60131
|
+
|
|
60132
|
+
float denom = d2121 * d4343 - d4321 * d4321;
|
|
60133
|
+
|
|
60134
|
+
float numer = d1343 * d4321 - d1321 * d4343;
|
|
60135
|
+
|
|
60136
|
+
mua = numer / denom;
|
|
60137
|
+
mua = clamp( mua, 0.0, 1.0 );
|
|
60138
|
+
mub = ( d1343 + d4321 * ( mua ) ) / d4343;
|
|
60139
|
+
mub = clamp( mub, 0.0, 1.0 );
|
|
60140
|
+
|
|
60141
|
+
return vec2( mua, mub );
|
|
60142
|
+
|
|
60143
|
+
}
|
|
60144
|
+
|
|
60145
|
+
void main() {
|
|
60146
|
+
|
|
60147
|
+
#include <clipping_planes_fragment>
|
|
60148
|
+
|
|
60149
|
+
#ifdef USE_DASH
|
|
60150
|
+
|
|
60151
|
+
if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps
|
|
60152
|
+
|
|
60153
|
+
if ( mod( vLineDistance + dashOffset, dashSize + gapSize ) > dashSize ) discard; // todo - FIX
|
|
60154
|
+
|
|
60155
|
+
#endif
|
|
60156
|
+
|
|
60157
|
+
float alpha = opacity;
|
|
60158
|
+
|
|
60159
|
+
#ifdef WORLD_UNITS
|
|
60160
|
+
|
|
60161
|
+
// Find the closest points on the view ray and the line segment
|
|
60162
|
+
vec3 rayEnd = normalize( worldPos.xyz ) * 1e5;
|
|
60163
|
+
vec3 lineDir = worldEnd - worldStart;
|
|
60164
|
+
vec2 params = closestLineToLine( worldStart, worldEnd, vec3( 0.0, 0.0, 0.0 ), rayEnd );
|
|
60165
|
+
|
|
60166
|
+
vec3 p1 = worldStart + lineDir * params.x;
|
|
60167
|
+
vec3 p2 = rayEnd * params.y;
|
|
60168
|
+
vec3 delta = p1 - p2;
|
|
60169
|
+
float len = length( delta );
|
|
60170
|
+
float norm = len / linewidth;
|
|
60171
|
+
|
|
60172
|
+
#ifndef USE_DASH
|
|
60173
|
+
|
|
60174
|
+
#ifdef USE_ALPHA_TO_COVERAGE
|
|
60175
|
+
|
|
60176
|
+
float dnorm = fwidth( norm );
|
|
60177
|
+
alpha = 1.0 - smoothstep( 0.5 - dnorm, 0.5 + dnorm, norm );
|
|
60178
|
+
|
|
60179
|
+
#else
|
|
60180
|
+
|
|
60181
|
+
if ( norm > 0.5 ) {
|
|
60182
|
+
|
|
60183
|
+
discard;
|
|
60184
|
+
|
|
60185
|
+
}
|
|
60186
|
+
|
|
60187
|
+
#endif
|
|
60188
|
+
|
|
60189
|
+
#endif
|
|
60190
|
+
|
|
60191
|
+
#else
|
|
60192
|
+
|
|
60193
|
+
#ifdef USE_ALPHA_TO_COVERAGE
|
|
60194
|
+
|
|
60195
|
+
// artifacts appear on some hardware if a derivative is taken within a conditional
|
|
60196
|
+
float a = vUv.x;
|
|
60197
|
+
float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
|
|
60198
|
+
float len2 = a * a + b * b;
|
|
60199
|
+
float dlen = fwidth( len2 );
|
|
60200
|
+
|
|
60201
|
+
if ( abs( vUv.y ) > 1.0 ) {
|
|
60202
|
+
|
|
60203
|
+
alpha = 1.0 - smoothstep( 1.0 - dlen, 1.0 + dlen, len2 );
|
|
60204
|
+
|
|
60205
|
+
}
|
|
60206
|
+
|
|
60207
|
+
#else
|
|
60208
|
+
|
|
60209
|
+
if ( abs( vUv.y ) > 1.0 ) {
|
|
60210
|
+
|
|
60211
|
+
float a = vUv.x;
|
|
60212
|
+
float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
|
|
60213
|
+
float len2 = a * a + b * b;
|
|
60214
|
+
|
|
60215
|
+
if ( len2 > 1.0 ) discard;
|
|
60216
|
+
|
|
60217
|
+
}
|
|
60218
|
+
|
|
60219
|
+
#endif
|
|
60220
|
+
|
|
60221
|
+
#endif
|
|
60222
|
+
|
|
60223
|
+
vec4 diffuseColor = vec4( diffuse, alpha );
|
|
60224
|
+
|
|
60225
|
+
#include <logdepthbuf_fragment>
|
|
60226
|
+
#include <color_fragment>
|
|
60227
|
+
|
|
60228
|
+
gl_FragColor = vec4( diffuseColor.rgb, alpha );
|
|
60229
|
+
|
|
60230
|
+
#include <tonemapping_fragment>
|
|
60231
|
+
#include <colorspace_fragment>
|
|
60232
|
+
#include <fog_fragment>
|
|
60233
|
+
#include <premultiplied_alpha_fragment>
|
|
60234
|
+
|
|
60235
|
+
}
|
|
60236
|
+
`
|
|
60237
|
+
};
|
|
60238
|
+
|
|
60239
|
+
class LineMaterial extends ShaderMaterial {
|
|
60240
|
+
|
|
60241
|
+
|
|
60242
|
+
static get type() {
|
|
60243
|
+
|
|
60244
|
+
return 'LineMaterial';
|
|
60245
|
+
|
|
60246
|
+
}
|
|
60247
|
+
|
|
60248
|
+
constructor( parameters ) {
|
|
60249
|
+
|
|
60250
|
+
super( {
|
|
60251
|
+
|
|
60252
|
+
uniforms: UniformsUtils.clone( ShaderLib[ 'line' ].uniforms ),
|
|
60253
|
+
|
|
60254
|
+
vertexShader: ShaderLib[ 'line' ].vertexShader,
|
|
60255
|
+
fragmentShader: ShaderLib[ 'line' ].fragmentShader,
|
|
60256
|
+
|
|
60257
|
+
clipping: true // required for clipping support
|
|
60258
|
+
|
|
60259
|
+
} );
|
|
60260
|
+
|
|
60261
|
+
this.isLineMaterial = true;
|
|
60262
|
+
|
|
60263
|
+
this.setValues( parameters );
|
|
60264
|
+
|
|
60265
|
+
}
|
|
60266
|
+
|
|
60267
|
+
get color() {
|
|
60268
|
+
|
|
60269
|
+
return this.uniforms.diffuse.value;
|
|
60270
|
+
|
|
60271
|
+
}
|
|
60272
|
+
|
|
60273
|
+
set color( value ) {
|
|
60274
|
+
|
|
60275
|
+
this.uniforms.diffuse.value = value;
|
|
60276
|
+
|
|
60277
|
+
}
|
|
60278
|
+
|
|
60279
|
+
get worldUnits() {
|
|
60280
|
+
|
|
60281
|
+
return 'WORLD_UNITS' in this.defines;
|
|
60282
|
+
|
|
60283
|
+
}
|
|
60284
|
+
|
|
60285
|
+
set worldUnits( value ) {
|
|
60286
|
+
|
|
60287
|
+
if ( value === true ) {
|
|
60288
|
+
|
|
60289
|
+
this.defines.WORLD_UNITS = '';
|
|
60290
|
+
|
|
60291
|
+
} else {
|
|
60292
|
+
|
|
60293
|
+
delete this.defines.WORLD_UNITS;
|
|
60294
|
+
|
|
60295
|
+
}
|
|
60296
|
+
|
|
60297
|
+
}
|
|
60298
|
+
|
|
60299
|
+
get linewidth() {
|
|
60300
|
+
|
|
60301
|
+
return this.uniforms.linewidth.value;
|
|
60302
|
+
|
|
60303
|
+
}
|
|
60304
|
+
|
|
60305
|
+
set linewidth( value ) {
|
|
60306
|
+
|
|
60307
|
+
if ( ! this.uniforms.linewidth ) return;
|
|
60308
|
+
this.uniforms.linewidth.value = value;
|
|
60309
|
+
|
|
60310
|
+
}
|
|
60311
|
+
|
|
60312
|
+
get dashed() {
|
|
60313
|
+
|
|
60314
|
+
return 'USE_DASH' in this.defines;
|
|
60315
|
+
|
|
60316
|
+
}
|
|
60317
|
+
|
|
60318
|
+
set dashed( value ) {
|
|
60319
|
+
|
|
60320
|
+
if ( ( value === true ) !== this.dashed ) {
|
|
60321
|
+
|
|
60322
|
+
this.needsUpdate = true;
|
|
60323
|
+
|
|
60324
|
+
}
|
|
60325
|
+
|
|
60326
|
+
if ( value === true ) {
|
|
60327
|
+
|
|
60328
|
+
this.defines.USE_DASH = '';
|
|
60329
|
+
|
|
60330
|
+
} else {
|
|
60331
|
+
|
|
60332
|
+
delete this.defines.USE_DASH;
|
|
60333
|
+
|
|
60334
|
+
}
|
|
60335
|
+
|
|
60336
|
+
}
|
|
60337
|
+
|
|
60338
|
+
get dashScale() {
|
|
60339
|
+
|
|
60340
|
+
return this.uniforms.dashScale.value;
|
|
60341
|
+
|
|
60342
|
+
}
|
|
60343
|
+
|
|
60344
|
+
set dashScale( value ) {
|
|
60345
|
+
|
|
60346
|
+
this.uniforms.dashScale.value = value;
|
|
60347
|
+
|
|
60348
|
+
}
|
|
60349
|
+
|
|
60350
|
+
get dashSize() {
|
|
60351
|
+
|
|
60352
|
+
return this.uniforms.dashSize.value;
|
|
60353
|
+
|
|
60354
|
+
}
|
|
60355
|
+
|
|
60356
|
+
set dashSize( value ) {
|
|
60357
|
+
|
|
60358
|
+
this.uniforms.dashSize.value = value;
|
|
60359
|
+
|
|
60360
|
+
}
|
|
60361
|
+
|
|
60362
|
+
get dashOffset() {
|
|
60363
|
+
|
|
60364
|
+
return this.uniforms.dashOffset.value;
|
|
60365
|
+
|
|
60366
|
+
}
|
|
60367
|
+
|
|
60368
|
+
set dashOffset( value ) {
|
|
60369
|
+
|
|
60370
|
+
this.uniforms.dashOffset.value = value;
|
|
60371
|
+
|
|
60372
|
+
}
|
|
60373
|
+
|
|
60374
|
+
get gapSize() {
|
|
60375
|
+
|
|
60376
|
+
return this.uniforms.gapSize.value;
|
|
60377
|
+
|
|
60378
|
+
}
|
|
60379
|
+
|
|
60380
|
+
set gapSize( value ) {
|
|
60381
|
+
|
|
60382
|
+
this.uniforms.gapSize.value = value;
|
|
60383
|
+
|
|
60384
|
+
}
|
|
60385
|
+
|
|
60386
|
+
get opacity() {
|
|
60387
|
+
|
|
60388
|
+
return this.uniforms.opacity.value;
|
|
60389
|
+
|
|
60390
|
+
}
|
|
60391
|
+
|
|
60392
|
+
set opacity( value ) {
|
|
60393
|
+
|
|
60394
|
+
if ( ! this.uniforms ) return;
|
|
60395
|
+
this.uniforms.opacity.value = value;
|
|
60396
|
+
|
|
60397
|
+
}
|
|
60398
|
+
|
|
60399
|
+
get resolution() {
|
|
60400
|
+
|
|
60401
|
+
return this.uniforms.resolution.value;
|
|
60402
|
+
|
|
60403
|
+
}
|
|
60404
|
+
|
|
60405
|
+
set resolution( value ) {
|
|
60406
|
+
|
|
60407
|
+
this.uniforms.resolution.value.copy( value );
|
|
60408
|
+
|
|
60409
|
+
}
|
|
60410
|
+
|
|
60411
|
+
get alphaToCoverage() {
|
|
60412
|
+
|
|
60413
|
+
return 'USE_ALPHA_TO_COVERAGE' in this.defines;
|
|
60414
|
+
|
|
60415
|
+
}
|
|
60416
|
+
|
|
60417
|
+
set alphaToCoverage( value ) {
|
|
60418
|
+
|
|
60419
|
+
if ( ! this.defines ) return;
|
|
60420
|
+
|
|
60421
|
+
if ( ( value === true ) !== this.alphaToCoverage ) {
|
|
60422
|
+
|
|
60423
|
+
this.needsUpdate = true;
|
|
60424
|
+
|
|
60425
|
+
}
|
|
60426
|
+
|
|
60427
|
+
if ( value === true ) {
|
|
60428
|
+
|
|
60429
|
+
this.defines.USE_ALPHA_TO_COVERAGE = '';
|
|
60430
|
+
|
|
60431
|
+
} else {
|
|
60432
|
+
|
|
60433
|
+
delete this.defines.USE_ALPHA_TO_COVERAGE;
|
|
60434
|
+
|
|
60435
|
+
}
|
|
60436
|
+
|
|
60437
|
+
}
|
|
60438
|
+
|
|
60439
|
+
}
|
|
60440
|
+
|
|
60441
|
+
const _viewport = new Vector4();
|
|
60442
|
+
|
|
60443
|
+
const _start = new Vector3();
|
|
60444
|
+
const _end = new Vector3();
|
|
60445
|
+
|
|
60446
|
+
const _start4 = new Vector4();
|
|
60447
|
+
const _end4 = new Vector4();
|
|
60448
|
+
|
|
60449
|
+
const _ssOrigin = new Vector4();
|
|
60450
|
+
const _ssOrigin3 = new Vector3();
|
|
60451
|
+
const _mvMatrix = new Matrix4();
|
|
60452
|
+
const _line = new Line3();
|
|
60453
|
+
const _closestPoint = new Vector3();
|
|
60454
|
+
|
|
60455
|
+
const _box = new Box3();
|
|
60456
|
+
const _sphere = new Sphere();
|
|
60457
|
+
const _clipToWorldVector = new Vector4();
|
|
60458
|
+
|
|
60459
|
+
let _ray, _lineWidth;
|
|
60460
|
+
|
|
60461
|
+
// Returns the margin required to expand by in world space given the distance from the camera,
|
|
60462
|
+
// line width, resolution, and camera projection
|
|
60463
|
+
function getWorldSpaceHalfWidth( camera, distance, resolution ) {
|
|
60464
|
+
|
|
60465
|
+
// transform into clip space, adjust the x and y values by the pixel width offset, then
|
|
60466
|
+
// transform back into world space to get world offset. Note clip space is [-1, 1] so full
|
|
60467
|
+
// width does not need to be halved.
|
|
60468
|
+
_clipToWorldVector.set( 0, 0, - distance, 1.0 ).applyMatrix4( camera.projectionMatrix );
|
|
60469
|
+
_clipToWorldVector.multiplyScalar( 1.0 / _clipToWorldVector.w );
|
|
60470
|
+
_clipToWorldVector.x = _lineWidth / resolution.width;
|
|
60471
|
+
_clipToWorldVector.y = _lineWidth / resolution.height;
|
|
60472
|
+
_clipToWorldVector.applyMatrix4( camera.projectionMatrixInverse );
|
|
60473
|
+
_clipToWorldVector.multiplyScalar( 1.0 / _clipToWorldVector.w );
|
|
60474
|
+
|
|
60475
|
+
return Math.abs( Math.max( _clipToWorldVector.x, _clipToWorldVector.y ) );
|
|
60476
|
+
|
|
60477
|
+
}
|
|
60478
|
+
|
|
60479
|
+
function raycastWorldUnits( lineSegments, intersects ) {
|
|
60480
|
+
|
|
60481
|
+
const matrixWorld = lineSegments.matrixWorld;
|
|
60482
|
+
const geometry = lineSegments.geometry;
|
|
60483
|
+
const instanceStart = geometry.attributes.instanceStart;
|
|
60484
|
+
const instanceEnd = geometry.attributes.instanceEnd;
|
|
60485
|
+
const segmentCount = Math.min( geometry.instanceCount, instanceStart.count );
|
|
60486
|
+
|
|
60487
|
+
for ( let i = 0, l = segmentCount; i < l; i ++ ) {
|
|
60488
|
+
|
|
60489
|
+
_line.start.fromBufferAttribute( instanceStart, i );
|
|
60490
|
+
_line.end.fromBufferAttribute( instanceEnd, i );
|
|
60491
|
+
|
|
60492
|
+
_line.applyMatrix4( matrixWorld );
|
|
60493
|
+
|
|
60494
|
+
const pointOnLine = new Vector3();
|
|
60495
|
+
const point = new Vector3();
|
|
60496
|
+
|
|
60497
|
+
_ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine );
|
|
60498
|
+
const isInside = point.distanceTo( pointOnLine ) < _lineWidth * 0.5;
|
|
60499
|
+
|
|
60500
|
+
if ( isInside ) {
|
|
60501
|
+
|
|
60502
|
+
intersects.push( {
|
|
60503
|
+
point,
|
|
60504
|
+
pointOnLine,
|
|
60505
|
+
distance: _ray.origin.distanceTo( point ),
|
|
60506
|
+
object: lineSegments,
|
|
60507
|
+
face: null,
|
|
60508
|
+
faceIndex: i,
|
|
60509
|
+
uv: null,
|
|
60510
|
+
uv1: null,
|
|
60511
|
+
} );
|
|
60512
|
+
|
|
60513
|
+
}
|
|
60514
|
+
|
|
60515
|
+
}
|
|
60516
|
+
|
|
60517
|
+
}
|
|
60518
|
+
|
|
60519
|
+
function raycastScreenSpace( lineSegments, camera, intersects ) {
|
|
60520
|
+
|
|
60521
|
+
const projectionMatrix = camera.projectionMatrix;
|
|
60522
|
+
const material = lineSegments.material;
|
|
60523
|
+
const resolution = material.resolution;
|
|
60524
|
+
const matrixWorld = lineSegments.matrixWorld;
|
|
60525
|
+
|
|
60526
|
+
const geometry = lineSegments.geometry;
|
|
60527
|
+
const instanceStart = geometry.attributes.instanceStart;
|
|
60528
|
+
const instanceEnd = geometry.attributes.instanceEnd;
|
|
60529
|
+
const segmentCount = Math.min( geometry.instanceCount, instanceStart.count );
|
|
60530
|
+
|
|
60531
|
+
const near = - camera.near;
|
|
60532
|
+
|
|
60533
|
+
//
|
|
60534
|
+
|
|
60535
|
+
// pick a point 1 unit out along the ray to avoid the ray origin
|
|
60536
|
+
// sitting at the camera origin which will cause "w" to be 0 when
|
|
60537
|
+
// applying the projection matrix.
|
|
60538
|
+
_ray.at( 1, _ssOrigin );
|
|
60539
|
+
|
|
60540
|
+
// ndc space [ - 1.0, 1.0 ]
|
|
60541
|
+
_ssOrigin.w = 1;
|
|
60542
|
+
_ssOrigin.applyMatrix4( camera.matrixWorldInverse );
|
|
60543
|
+
_ssOrigin.applyMatrix4( projectionMatrix );
|
|
60544
|
+
_ssOrigin.multiplyScalar( 1 / _ssOrigin.w );
|
|
60545
|
+
|
|
60546
|
+
// screen space
|
|
60547
|
+
_ssOrigin.x *= resolution.x / 2;
|
|
60548
|
+
_ssOrigin.y *= resolution.y / 2;
|
|
60549
|
+
_ssOrigin.z = 0;
|
|
60550
|
+
|
|
60551
|
+
_ssOrigin3.copy( _ssOrigin );
|
|
60552
|
+
|
|
60553
|
+
_mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld );
|
|
60554
|
+
|
|
60555
|
+
for ( let i = 0, l = segmentCount; i < l; i ++ ) {
|
|
60556
|
+
|
|
60557
|
+
_start4.fromBufferAttribute( instanceStart, i );
|
|
60558
|
+
_end4.fromBufferAttribute( instanceEnd, i );
|
|
60559
|
+
|
|
60560
|
+
_start4.w = 1;
|
|
60561
|
+
_end4.w = 1;
|
|
60562
|
+
|
|
60563
|
+
// camera space
|
|
60564
|
+
_start4.applyMatrix4( _mvMatrix );
|
|
60565
|
+
_end4.applyMatrix4( _mvMatrix );
|
|
60566
|
+
|
|
60567
|
+
// skip the segment if it's entirely behind the camera
|
|
60568
|
+
const isBehindCameraNear = _start4.z > near && _end4.z > near;
|
|
60569
|
+
if ( isBehindCameraNear ) {
|
|
60570
|
+
|
|
60571
|
+
continue;
|
|
60572
|
+
|
|
60573
|
+
}
|
|
60574
|
+
|
|
60575
|
+
// trim the segment if it extends behind camera near
|
|
60576
|
+
if ( _start4.z > near ) {
|
|
60577
|
+
|
|
60578
|
+
const deltaDist = _start4.z - _end4.z;
|
|
60579
|
+
const t = ( _start4.z - near ) / deltaDist;
|
|
60580
|
+
_start4.lerp( _end4, t );
|
|
60581
|
+
|
|
60582
|
+
} else if ( _end4.z > near ) {
|
|
60583
|
+
|
|
60584
|
+
const deltaDist = _end4.z - _start4.z;
|
|
60585
|
+
const t = ( _end4.z - near ) / deltaDist;
|
|
60586
|
+
_end4.lerp( _start4, t );
|
|
60587
|
+
|
|
60588
|
+
}
|
|
60589
|
+
|
|
60590
|
+
// clip space
|
|
60591
|
+
_start4.applyMatrix4( projectionMatrix );
|
|
60592
|
+
_end4.applyMatrix4( projectionMatrix );
|
|
60593
|
+
|
|
60594
|
+
// ndc space [ - 1.0, 1.0 ]
|
|
60595
|
+
_start4.multiplyScalar( 1 / _start4.w );
|
|
60596
|
+
_end4.multiplyScalar( 1 / _end4.w );
|
|
60597
|
+
|
|
60598
|
+
// screen space
|
|
60599
|
+
_start4.x *= resolution.x / 2;
|
|
60600
|
+
_start4.y *= resolution.y / 2;
|
|
60601
|
+
|
|
60602
|
+
_end4.x *= resolution.x / 2;
|
|
60603
|
+
_end4.y *= resolution.y / 2;
|
|
60604
|
+
|
|
60605
|
+
// create 2d segment
|
|
60606
|
+
_line.start.copy( _start4 );
|
|
60607
|
+
_line.start.z = 0;
|
|
60608
|
+
|
|
60609
|
+
_line.end.copy( _end4 );
|
|
60610
|
+
_line.end.z = 0;
|
|
60611
|
+
|
|
60612
|
+
// get closest point on ray to segment
|
|
60613
|
+
const param = _line.closestPointToPointParameter( _ssOrigin3, true );
|
|
60614
|
+
_line.at( param, _closestPoint );
|
|
60615
|
+
|
|
60616
|
+
// check if the intersection point is within clip space
|
|
60617
|
+
const zPos = MathUtils.lerp( _start4.z, _end4.z, param );
|
|
60618
|
+
const isInClipSpace = zPos >= - 1 && zPos <= 1;
|
|
60619
|
+
|
|
60620
|
+
const isInside = _ssOrigin3.distanceTo( _closestPoint ) < _lineWidth * 0.5;
|
|
60621
|
+
|
|
60622
|
+
if ( isInClipSpace && isInside ) {
|
|
60623
|
+
|
|
60624
|
+
_line.start.fromBufferAttribute( instanceStart, i );
|
|
60625
|
+
_line.end.fromBufferAttribute( instanceEnd, i );
|
|
60626
|
+
|
|
60627
|
+
_line.start.applyMatrix4( matrixWorld );
|
|
60628
|
+
_line.end.applyMatrix4( matrixWorld );
|
|
60629
|
+
|
|
60630
|
+
const pointOnLine = new Vector3();
|
|
60631
|
+
const point = new Vector3();
|
|
60632
|
+
|
|
60633
|
+
_ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine );
|
|
60634
|
+
|
|
60635
|
+
intersects.push( {
|
|
60636
|
+
point: point,
|
|
60637
|
+
pointOnLine: pointOnLine,
|
|
60638
|
+
distance: _ray.origin.distanceTo( point ),
|
|
60639
|
+
object: lineSegments,
|
|
60640
|
+
face: null,
|
|
60641
|
+
faceIndex: i,
|
|
60642
|
+
uv: null,
|
|
60643
|
+
uv1: null,
|
|
60644
|
+
} );
|
|
60645
|
+
|
|
60646
|
+
}
|
|
60647
|
+
|
|
60648
|
+
}
|
|
60649
|
+
|
|
60650
|
+
}
|
|
60651
|
+
|
|
60652
|
+
class LineSegments2 extends Mesh {
|
|
60653
|
+
|
|
60654
|
+
constructor( geometry = new LineSegmentsGeometry(), material = new LineMaterial( { color: Math.random() * 0xffffff } ) ) {
|
|
60655
|
+
|
|
60656
|
+
super( geometry, material );
|
|
60657
|
+
|
|
60658
|
+
this.isLineSegments2 = true;
|
|
60659
|
+
|
|
60660
|
+
this.type = 'LineSegments2';
|
|
60661
|
+
|
|
60662
|
+
}
|
|
60663
|
+
|
|
60664
|
+
// for backwards-compatibility, but could be a method of LineSegmentsGeometry...
|
|
60665
|
+
|
|
60666
|
+
computeLineDistances() {
|
|
60667
|
+
|
|
60668
|
+
const geometry = this.geometry;
|
|
60669
|
+
|
|
60670
|
+
const instanceStart = geometry.attributes.instanceStart;
|
|
60671
|
+
const instanceEnd = geometry.attributes.instanceEnd;
|
|
60672
|
+
const lineDistances = new Float32Array( 2 * instanceStart.count );
|
|
60673
|
+
|
|
60674
|
+
for ( let i = 0, j = 0, l = instanceStart.count; i < l; i ++, j += 2 ) {
|
|
60675
|
+
|
|
60676
|
+
_start.fromBufferAttribute( instanceStart, i );
|
|
60677
|
+
_end.fromBufferAttribute( instanceEnd, i );
|
|
60678
|
+
|
|
60679
|
+
lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ];
|
|
60680
|
+
lineDistances[ j + 1 ] = lineDistances[ j ] + _start.distanceTo( _end );
|
|
60681
|
+
|
|
60682
|
+
}
|
|
60683
|
+
|
|
60684
|
+
const instanceDistanceBuffer = new InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1
|
|
60685
|
+
|
|
60686
|
+
geometry.setAttribute( 'instanceDistanceStart', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0
|
|
60687
|
+
geometry.setAttribute( 'instanceDistanceEnd', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1
|
|
60688
|
+
|
|
60689
|
+
return this;
|
|
60690
|
+
|
|
60691
|
+
}
|
|
60692
|
+
|
|
60693
|
+
raycast( raycaster, intersects ) {
|
|
60694
|
+
|
|
60695
|
+
const worldUnits = this.material.worldUnits;
|
|
60696
|
+
const camera = raycaster.camera;
|
|
60697
|
+
|
|
60698
|
+
if ( camera === null && ! worldUnits ) {
|
|
60699
|
+
|
|
60700
|
+
console.error( 'LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2 while worldUnits is set to false.' );
|
|
60701
|
+
|
|
60702
|
+
}
|
|
60703
|
+
|
|
60704
|
+
const threshold = ( raycaster.params.Line2 !== undefined ) ? raycaster.params.Line2.threshold || 0 : 0;
|
|
60705
|
+
|
|
60706
|
+
_ray = raycaster.ray;
|
|
60707
|
+
|
|
60708
|
+
const matrixWorld = this.matrixWorld;
|
|
60709
|
+
const geometry = this.geometry;
|
|
60710
|
+
const material = this.material;
|
|
60711
|
+
|
|
60712
|
+
_lineWidth = material.linewidth + threshold;
|
|
60713
|
+
|
|
60714
|
+
// check if we intersect the sphere bounds
|
|
60715
|
+
if ( geometry.boundingSphere === null ) {
|
|
60716
|
+
|
|
60717
|
+
geometry.computeBoundingSphere();
|
|
60718
|
+
|
|
60719
|
+
}
|
|
60720
|
+
|
|
60721
|
+
_sphere.copy( geometry.boundingSphere ).applyMatrix4( matrixWorld );
|
|
60722
|
+
|
|
60723
|
+
// increase the sphere bounds by the worst case line screen space width
|
|
60724
|
+
let sphereMargin;
|
|
60725
|
+
if ( worldUnits ) {
|
|
60726
|
+
|
|
60727
|
+
sphereMargin = _lineWidth * 0.5;
|
|
60728
|
+
|
|
60729
|
+
} else {
|
|
60730
|
+
|
|
60731
|
+
const distanceToSphere = Math.max( camera.near, _sphere.distanceToPoint( _ray.origin ) );
|
|
60732
|
+
sphereMargin = getWorldSpaceHalfWidth( camera, distanceToSphere, material.resolution );
|
|
60733
|
+
|
|
60734
|
+
}
|
|
60735
|
+
|
|
60736
|
+
_sphere.radius += sphereMargin;
|
|
60737
|
+
|
|
60738
|
+
if ( _ray.intersectsSphere( _sphere ) === false ) {
|
|
60739
|
+
|
|
60740
|
+
return;
|
|
60741
|
+
|
|
60742
|
+
}
|
|
60743
|
+
|
|
60744
|
+
// check if we intersect the box bounds
|
|
60745
|
+
if ( geometry.boundingBox === null ) {
|
|
60746
|
+
|
|
60747
|
+
geometry.computeBoundingBox();
|
|
60748
|
+
|
|
60749
|
+
}
|
|
58684
60750
|
|
|
58685
|
-
|
|
58686
|
-
this._zoomEnd.copy( this._zoomStart );
|
|
60751
|
+
_box.copy( geometry.boundingBox ).applyMatrix4( matrixWorld );
|
|
58687
60752
|
|
|
58688
|
-
|
|
60753
|
+
// increase the box bounds by the worst case line width
|
|
60754
|
+
let boxMargin;
|
|
60755
|
+
if ( worldUnits ) {
|
|
58689
60756
|
|
|
58690
|
-
|
|
58691
|
-
this._panEnd.copy( this._panStart );
|
|
60757
|
+
boxMargin = _lineWidth * 0.5;
|
|
58692
60758
|
|
|
58693
|
-
|
|
60759
|
+
} else {
|
|
58694
60760
|
|
|
58695
|
-
|
|
60761
|
+
const distanceToBox = Math.max( camera.near, _box.distanceToPoint( _ray.origin ) );
|
|
60762
|
+
boxMargin = getWorldSpaceHalfWidth( camera, distanceToBox, material.resolution );
|
|
58696
60763
|
|
|
58697
|
-
}
|
|
60764
|
+
}
|
|
58698
60765
|
|
|
58699
|
-
|
|
60766
|
+
_box.expandByScalar( boxMargin );
|
|
58700
60767
|
|
|
58701
|
-
|
|
60768
|
+
if ( _ray.intersectsBox( _box ) === false ) {
|
|
58702
60769
|
|
|
58703
|
-
|
|
60770
|
+
return;
|
|
58704
60771
|
|
|
58705
|
-
|
|
58706
|
-
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
60772
|
+
}
|
|
58707
60773
|
|
|
58708
|
-
|
|
60774
|
+
if ( worldUnits ) {
|
|
58709
60775
|
|
|
58710
|
-
|
|
60776
|
+
raycastWorldUnits( this, intersects );
|
|
58711
60777
|
|
|
58712
|
-
|
|
60778
|
+
} else {
|
|
58713
60779
|
|
|
58714
|
-
|
|
60780
|
+
raycastScreenSpace( this, camera, intersects );
|
|
58715
60781
|
|
|
58716
|
-
|
|
60782
|
+
}
|
|
58717
60783
|
|
|
58718
|
-
}
|
|
60784
|
+
}
|
|
58719
60785
|
|
|
58720
|
-
|
|
60786
|
+
onBeforeRender( renderer ) {
|
|
58721
60787
|
|
|
58722
|
-
|
|
60788
|
+
const uniforms = this.material.uniforms;
|
|
58723
60789
|
|
|
58724
|
-
|
|
60790
|
+
if ( uniforms && uniforms.resolution ) {
|
|
58725
60791
|
|
|
58726
|
-
|
|
60792
|
+
renderer.getViewport( _viewport );
|
|
60793
|
+
this.material.uniforms.resolution.value.set( _viewport.z, _viewport.w );
|
|
58727
60794
|
|
|
58728
|
-
|
|
60795
|
+
}
|
|
58729
60796
|
|
|
58730
|
-
|
|
60797
|
+
}
|
|
58731
60798
|
|
|
58732
|
-
|
|
60799
|
+
}
|
|
58733
60800
|
|
|
58734
|
-
|
|
60801
|
+
class LineGeometry extends LineSegmentsGeometry {
|
|
58735
60802
|
|
|
58736
|
-
|
|
60803
|
+
constructor() {
|
|
58737
60804
|
|
|
58738
|
-
|
|
58739
|
-
// Zoom in pages
|
|
58740
|
-
this._zoomStart.y -= event.deltaY * 0.025;
|
|
58741
|
-
break;
|
|
60805
|
+
super();
|
|
58742
60806
|
|
|
58743
|
-
|
|
58744
|
-
// Zoom in lines
|
|
58745
|
-
this._zoomStart.y -= event.deltaY * 0.01;
|
|
58746
|
-
break;
|
|
60807
|
+
this.isLineGeometry = true;
|
|
58747
60808
|
|
|
58748
|
-
|
|
58749
|
-
// undefined, 0, assume pixels
|
|
58750
|
-
this._zoomStart.y -= event.deltaY * 0.00025;
|
|
58751
|
-
break;
|
|
60809
|
+
this.type = 'LineGeometry';
|
|
58752
60810
|
|
|
58753
60811
|
}
|
|
58754
60812
|
|
|
58755
|
-
|
|
58756
|
-
this.dispatchEvent( _endEvent );
|
|
60813
|
+
setPositions( array ) {
|
|
58757
60814
|
|
|
58758
|
-
|
|
60815
|
+
// converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format
|
|
58759
60816
|
|
|
58760
|
-
|
|
60817
|
+
const length = array.length - 3;
|
|
60818
|
+
const points = new Float32Array( 2 * length );
|
|
58761
60819
|
|
|
58762
|
-
|
|
60820
|
+
for ( let i = 0; i < length; i += 3 ) {
|
|
58763
60821
|
|
|
58764
|
-
|
|
60822
|
+
points[ 2 * i ] = array[ i ];
|
|
60823
|
+
points[ 2 * i + 1 ] = array[ i + 1 ];
|
|
60824
|
+
points[ 2 * i + 2 ] = array[ i + 2 ];
|
|
58765
60825
|
|
|
58766
|
-
|
|
60826
|
+
points[ 2 * i + 3 ] = array[ i + 3 ];
|
|
60827
|
+
points[ 2 * i + 4 ] = array[ i + 4 ];
|
|
60828
|
+
points[ 2 * i + 5 ] = array[ i + 5 ];
|
|
58767
60829
|
|
|
58768
|
-
|
|
60830
|
+
}
|
|
58769
60831
|
|
|
58770
|
-
|
|
60832
|
+
super.setPositions( points );
|
|
58771
60833
|
|
|
58772
|
-
|
|
60834
|
+
return this;
|
|
58773
60835
|
|
|
58774
|
-
|
|
58775
|
-
this.state = _STATE.TOUCH_ROTATE;
|
|
58776
|
-
this._moveCurr.copy( this._getMouseOnCircle( this._pointers[ 0 ].pageX, this._pointers[ 0 ].pageY ) );
|
|
58777
|
-
this._movePrev.copy( this._moveCurr );
|
|
58778
|
-
break;
|
|
60836
|
+
}
|
|
58779
60837
|
|
|
58780
|
-
|
|
58781
|
-
this.state = _STATE.TOUCH_ZOOM_PAN;
|
|
58782
|
-
const dx = this._pointers[ 0 ].pageX - this._pointers[ 1 ].pageX;
|
|
58783
|
-
const dy = this._pointers[ 0 ].pageY - this._pointers[ 1 ].pageY;
|
|
58784
|
-
this._touchZoomDistanceEnd = this._touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
|
|
60838
|
+
setColors( array ) {
|
|
58785
60839
|
|
|
58786
|
-
|
|
58787
|
-
const y = ( this._pointers[ 0 ].pageY + this._pointers[ 1 ].pageY ) / 2;
|
|
58788
|
-
this._panStart.copy( this._getMouseOnScreen( x, y ) );
|
|
58789
|
-
this._panEnd.copy( this._panStart );
|
|
58790
|
-
break;
|
|
60840
|
+
// converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format
|
|
58791
60841
|
|
|
58792
|
-
|
|
60842
|
+
const length = array.length - 3;
|
|
60843
|
+
const colors = new Float32Array( 2 * length );
|
|
58793
60844
|
|
|
58794
|
-
|
|
60845
|
+
for ( let i = 0; i < length; i += 3 ) {
|
|
58795
60846
|
|
|
58796
|
-
|
|
60847
|
+
colors[ 2 * i ] = array[ i ];
|
|
60848
|
+
colors[ 2 * i + 1 ] = array[ i + 1 ];
|
|
60849
|
+
colors[ 2 * i + 2 ] = array[ i + 2 ];
|
|
58797
60850
|
|
|
58798
|
-
|
|
60851
|
+
colors[ 2 * i + 3 ] = array[ i + 3 ];
|
|
60852
|
+
colors[ 2 * i + 4 ] = array[ i + 4 ];
|
|
60853
|
+
colors[ 2 * i + 5 ] = array[ i + 5 ];
|
|
58799
60854
|
|
|
58800
|
-
|
|
60855
|
+
}
|
|
58801
60856
|
|
|
58802
|
-
|
|
60857
|
+
super.setColors( colors );
|
|
58803
60858
|
|
|
58804
|
-
|
|
58805
|
-
this._movePrev.copy( this._moveCurr );
|
|
58806
|
-
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
58807
|
-
break;
|
|
60859
|
+
return this;
|
|
58808
60860
|
|
|
58809
|
-
|
|
60861
|
+
}
|
|
58810
60862
|
|
|
58811
|
-
|
|
60863
|
+
fromLine( line ) {
|
|
58812
60864
|
|
|
58813
|
-
|
|
58814
|
-
const dy = event.pageY - position.y;
|
|
58815
|
-
this._touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
|
|
60865
|
+
const geometry = line.geometry;
|
|
58816
60866
|
|
|
58817
|
-
|
|
58818
|
-
const y = ( event.pageY + position.y ) / 2;
|
|
58819
|
-
this._panEnd.copy( this._getMouseOnScreen( x, y ) );
|
|
58820
|
-
break;
|
|
60867
|
+
this.setPositions( geometry.attributes.position.array ); // assumes non-indexed
|
|
58821
60868
|
|
|
58822
|
-
|
|
60869
|
+
// set colors, maybe
|
|
58823
60870
|
|
|
58824
|
-
|
|
60871
|
+
return this;
|
|
58825
60872
|
|
|
58826
|
-
|
|
60873
|
+
}
|
|
58827
60874
|
|
|
58828
|
-
|
|
60875
|
+
}
|
|
58829
60876
|
|
|
58830
|
-
|
|
58831
|
-
this.state = _STATE.NONE;
|
|
58832
|
-
break;
|
|
60877
|
+
class Line2 extends LineSegments2 {
|
|
58833
60878
|
|
|
58834
|
-
|
|
58835
|
-
this.state = _STATE.TOUCH_ROTATE;
|
|
58836
|
-
this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) );
|
|
58837
|
-
this._movePrev.copy( this._moveCurr );
|
|
58838
|
-
break;
|
|
60879
|
+
constructor( geometry = new LineGeometry(), material = new LineMaterial( { color: Math.random() * 0xffffff } ) ) {
|
|
58839
60880
|
|
|
58840
|
-
|
|
58841
|
-
this.state = _STATE.TOUCH_ZOOM_PAN;
|
|
60881
|
+
super( geometry, material );
|
|
58842
60882
|
|
|
58843
|
-
|
|
60883
|
+
this.isLine2 = true;
|
|
58844
60884
|
|
|
58845
|
-
|
|
60885
|
+
this.type = 'Line2';
|
|
58846
60886
|
|
|
58847
|
-
|
|
58848
|
-
this._moveCurr.copy( this._getMouseOnCircle( position.x, position.y ) );
|
|
58849
|
-
this._movePrev.copy( this._moveCurr );
|
|
58850
|
-
break;
|
|
60887
|
+
}
|
|
58851
60888
|
|
|
58852
|
-
|
|
60889
|
+
}
|
|
58853
60890
|
|
|
58854
|
-
|
|
60891
|
+
/**
|
|
60892
|
+
* 把标注顶点(local)序列展平成 Line2 需要的 world [x,y,z, ...]。
|
|
60893
|
+
* 每个点 local→world,再沿 world 法线外移 epsilon,避免与模型表面 z-fighting。
|
|
60894
|
+
* closed 时把首点追加到末尾形成环。
|
|
60895
|
+
*/
|
|
60896
|
+
function flatten(verts, closed, epsilon, mesh) {
|
|
60897
|
+
const pts = [];
|
|
60898
|
+
const push = (v) => {
|
|
60899
|
+
const { p, n } = localVertexToWorld(v, mesh);
|
|
60900
|
+
pts.push(p.x + n.x * epsilon, p.y + n.y * epsilon, p.z + n.z * epsilon);
|
|
60901
|
+
};
|
|
60902
|
+
verts.forEach(push);
|
|
60903
|
+
if (closed && verts.length > 1)
|
|
60904
|
+
push(verts[0]);
|
|
60905
|
+
return pts;
|
|
60906
|
+
}
|
|
60907
|
+
function makeContourLine(verts, color, closed, container, epsilon, mesh) {
|
|
60908
|
+
const geo = new LineGeometry();
|
|
60909
|
+
const pos = flatten(verts, closed, epsilon, mesh);
|
|
60910
|
+
// Line2 至少需要 2 个点;不足时给一个退化占位,后续 update 会补上。
|
|
60911
|
+
geo.setPositions(pos.length >= 6 ? pos : [0, 0, 0, 0, 0, 0]);
|
|
60912
|
+
const mat = new LineMaterial({
|
|
60913
|
+
color: new Color$1(color).getHex(),
|
|
60914
|
+
linewidth: 3, // 像素宽(需 resolution 配合)
|
|
60915
|
+
});
|
|
60916
|
+
mat.resolution.set(container.clientWidth, container.clientHeight);
|
|
60917
|
+
const line = new Line2(geo, mat);
|
|
60918
|
+
line.computeLineDistances();
|
|
60919
|
+
line.renderOrder = 998;
|
|
60920
|
+
// 关键:Line2 的包围球停留在初始点,拖拽生长后真实几何会被错误剔除。
|
|
60921
|
+
// 关闭视锥剔除,保证线始终渲染。
|
|
60922
|
+
line.frustumCulled = false;
|
|
60923
|
+
return line;
|
|
60924
|
+
}
|
|
60925
|
+
function updateContourLine(line, verts, closed, epsilon, mesh) {
|
|
60926
|
+
const pos = flatten(verts, closed, epsilon, mesh);
|
|
60927
|
+
if (pos.length < 6)
|
|
60928
|
+
return; // Line2 需要 >=2 点
|
|
60929
|
+
// 关键:Line2 在原 geometry 上 setPositions 扩容后只渲染首段(已知坑)。
|
|
60930
|
+
// 改为每次重建一个新 LineGeometry 并替换,保证实例数正确、全段渲染。
|
|
60931
|
+
const geo = new LineGeometry();
|
|
60932
|
+
geo.setPositions(pos);
|
|
60933
|
+
const old = line.geometry;
|
|
60934
|
+
line.geometry = geo;
|
|
60935
|
+
old.dispose();
|
|
60936
|
+
line.computeLineDistances();
|
|
60937
|
+
}
|
|
60938
|
+
/** 改变已有 contour 线的颜色。 */
|
|
60939
|
+
function setContourColor(line, color) {
|
|
60940
|
+
line.material.color.set(color);
|
|
60941
|
+
}
|
|
58855
60942
|
|
|
58856
|
-
|
|
60943
|
+
/**
|
|
60944
|
+
* 从 BufferGeometry 的索引构建顶点邻接图,用于模式 B(测地线)的最短路径。
|
|
60945
|
+
* 顶点坐标按 local 存;传入的查询点需先转为 local(mesh.worldToLocal)。
|
|
60946
|
+
*
|
|
60947
|
+
* 说明:几何须为已索引(indexed)。modelLoader 用 mergeVertices 焊接重复顶点,
|
|
60948
|
+
* 保证同一表面位置共享顶点,邻接图才连通。
|
|
60949
|
+
*
|
|
60950
|
+
* 性能:O(V²) Dijkstra + O(V) 最近顶点查找。对几万顶点单次点击可接受;
|
|
60951
|
+
* 若过大可后续换二叉堆 + 空间网格加速(留作升级)。
|
|
60952
|
+
*/
|
|
60953
|
+
class MeshGraph {
|
|
60954
|
+
constructor(geometry) {
|
|
60955
|
+
this.adj = [];
|
|
60956
|
+
const pos = geometry.getAttribute("position");
|
|
60957
|
+
this.positions = pos.array;
|
|
60958
|
+
const nrm = geometry.getAttribute("normal");
|
|
60959
|
+
this.normals = nrm ? nrm.array : null;
|
|
60960
|
+
this.vertexCount = pos.count;
|
|
60961
|
+
for (let i = 0; i < this.vertexCount; i++)
|
|
60962
|
+
this.adj.push(new Set());
|
|
60963
|
+
const addEdge = (a, b) => {
|
|
60964
|
+
this.adj[a].add(b);
|
|
60965
|
+
this.adj[b].add(a);
|
|
60966
|
+
};
|
|
60967
|
+
const index = geometry.getIndex();
|
|
60968
|
+
if (index) {
|
|
60969
|
+
for (let i = 0; i < index.count; i += 3) {
|
|
60970
|
+
const a = index.getX(i);
|
|
60971
|
+
const b = index.getX(i + 1);
|
|
60972
|
+
const c = index.getX(i + 2);
|
|
60973
|
+
addEdge(a, b);
|
|
60974
|
+
addEdge(b, c);
|
|
60975
|
+
addEdge(c, a);
|
|
60976
|
+
}
|
|
60977
|
+
}
|
|
60978
|
+
else {
|
|
60979
|
+
// 非索引兜底(连通性差,仅避免崩溃)。
|
|
60980
|
+
for (let i = 0; i + 2 < this.vertexCount; i += 3) {
|
|
60981
|
+
addEdge(i, i + 1);
|
|
60982
|
+
addEdge(i + 1, i + 2);
|
|
60983
|
+
addEdge(i + 2, i);
|
|
60984
|
+
}
|
|
60985
|
+
}
|
|
60986
|
+
}
|
|
60987
|
+
px(i) {
|
|
60988
|
+
return this.positions[i * 3];
|
|
60989
|
+
}
|
|
60990
|
+
py(i) {
|
|
60991
|
+
return this.positions[i * 3 + 1];
|
|
60992
|
+
}
|
|
60993
|
+
pz(i) {
|
|
60994
|
+
return this.positions[i * 3 + 2];
|
|
60995
|
+
}
|
|
60996
|
+
/** 最近顶点(传入 local 坐标)。 */
|
|
60997
|
+
nearestVertex(localPoint) {
|
|
60998
|
+
let best = -1;
|
|
60999
|
+
let bestD = Infinity;
|
|
61000
|
+
for (let i = 0; i < this.vertexCount; i++) {
|
|
61001
|
+
const dx = this.px(i) - localPoint.x;
|
|
61002
|
+
const dy = this.py(i) - localPoint.y;
|
|
61003
|
+
const dz = this.pz(i) - localPoint.z;
|
|
61004
|
+
const d = dx * dx + dy * dy + dz * dz;
|
|
61005
|
+
if (d < bestD) {
|
|
61006
|
+
bestD = d;
|
|
61007
|
+
best = i;
|
|
61008
|
+
}
|
|
61009
|
+
}
|
|
61010
|
+
return best;
|
|
61011
|
+
}
|
|
61012
|
+
/**
|
|
61013
|
+
* 两顶点间最短路径(含端点的顶点索引序列)。二叉堆 Dijkstra,O(E log V)。
|
|
61014
|
+
* 不连通时返回 [start,end] 兜底。
|
|
61015
|
+
*/
|
|
61016
|
+
shortestPath(startV, endV) {
|
|
61017
|
+
if (startV === endV)
|
|
61018
|
+
return [startV];
|
|
61019
|
+
const dist = new Float64Array(this.vertexCount).fill(Infinity);
|
|
61020
|
+
const prev = new Int32Array(this.vertexCount).fill(-1);
|
|
61021
|
+
const done = new Uint8Array(this.vertexCount);
|
|
61022
|
+
dist[startV] = 0;
|
|
61023
|
+
const heap = new MinHeap();
|
|
61024
|
+
heap.push(startV, 0);
|
|
61025
|
+
while (heap.size > 0) {
|
|
61026
|
+
const u = heap.pop();
|
|
61027
|
+
if (done[u])
|
|
61028
|
+
continue;
|
|
61029
|
+
done[u] = 1;
|
|
61030
|
+
if (u === endV)
|
|
61031
|
+
break;
|
|
61032
|
+
const ud = dist[u];
|
|
61033
|
+
const ux = this.px(u);
|
|
61034
|
+
const uy = this.py(u);
|
|
61035
|
+
const uz = this.pz(u);
|
|
61036
|
+
for (const w of this.adj[u]) {
|
|
61037
|
+
if (done[w])
|
|
61038
|
+
continue;
|
|
61039
|
+
const dx = this.px(w) - ux;
|
|
61040
|
+
const dy = this.py(w) - uy;
|
|
61041
|
+
const dz = this.pz(w) - uz;
|
|
61042
|
+
const nd = ud + Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
61043
|
+
if (nd < dist[w]) {
|
|
61044
|
+
dist[w] = nd;
|
|
61045
|
+
prev[w] = u;
|
|
61046
|
+
heap.push(w, nd);
|
|
61047
|
+
}
|
|
61048
|
+
}
|
|
61049
|
+
}
|
|
61050
|
+
if (prev[endV] === -1 && startV !== endV)
|
|
61051
|
+
return [startV, endV];
|
|
61052
|
+
const path = [];
|
|
61053
|
+
for (let c = endV; c !== -1; c = prev[c]) {
|
|
61054
|
+
path.push(c);
|
|
61055
|
+
if (c === startV)
|
|
61056
|
+
break;
|
|
61057
|
+
}
|
|
61058
|
+
return path.reverse();
|
|
61059
|
+
}
|
|
61060
|
+
vertexWorld(i, matrixWorld) {
|
|
61061
|
+
return new Vector3(this.px(i), this.py(i), this.pz(i)).applyMatrix4(matrixWorld);
|
|
61062
|
+
}
|
|
61063
|
+
vertexNormalWorld(i, mesh) {
|
|
61064
|
+
const n = new Vector3(0, 0, 1);
|
|
61065
|
+
if (this.normals) {
|
|
61066
|
+
n.set(this.normals[i * 3], this.normals[i * 3 + 1], this.normals[i * 3 + 2]);
|
|
61067
|
+
}
|
|
61068
|
+
const nm = new Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
61069
|
+
return n.applyMatrix3(nm).normalize();
|
|
61070
|
+
}
|
|
61071
|
+
/** 顶点的 local 坐标 + local 法线(图几何即 local 空间)。 */
|
|
61072
|
+
vertexLocal(i) {
|
|
61073
|
+
return {
|
|
61074
|
+
x: this.px(i),
|
|
61075
|
+
y: this.py(i),
|
|
61076
|
+
z: this.pz(i),
|
|
61077
|
+
nx: this.normals ? this.normals[i * 3] : 0,
|
|
61078
|
+
ny: this.normals ? this.normals[i * 3 + 1] : 0,
|
|
61079
|
+
nz: this.normals ? this.normals[i * 3 + 2] : 1,
|
|
61080
|
+
faceIndex: -1,
|
|
61081
|
+
};
|
|
61082
|
+
}
|
|
61083
|
+
}
|
|
61084
|
+
/** 极简二叉最小堆(按 priority 排序,存顶点索引)。用于 Dijkstra。 */
|
|
61085
|
+
class MinHeap {
|
|
61086
|
+
constructor() {
|
|
61087
|
+
this.ids = [];
|
|
61088
|
+
this.prio = [];
|
|
61089
|
+
}
|
|
61090
|
+
get size() {
|
|
61091
|
+
return this.ids.length;
|
|
61092
|
+
}
|
|
61093
|
+
push(id, priority) {
|
|
61094
|
+
this.ids.push(id);
|
|
61095
|
+
this.prio.push(priority);
|
|
61096
|
+
let i = this.ids.length - 1;
|
|
61097
|
+
while (i > 0) {
|
|
61098
|
+
const parent = (i - 1) >> 1;
|
|
61099
|
+
if (this.prio[parent] <= this.prio[i])
|
|
61100
|
+
break;
|
|
61101
|
+
this.swap(i, parent);
|
|
61102
|
+
i = parent;
|
|
61103
|
+
}
|
|
61104
|
+
}
|
|
61105
|
+
pop() {
|
|
61106
|
+
const topId = this.ids[0];
|
|
61107
|
+
const lastId = this.ids.pop();
|
|
61108
|
+
const lastPrio = this.prio.pop();
|
|
61109
|
+
if (this.ids.length > 0) {
|
|
61110
|
+
this.ids[0] = lastId;
|
|
61111
|
+
this.prio[0] = lastPrio;
|
|
61112
|
+
let i = 0;
|
|
61113
|
+
const n = this.ids.length;
|
|
61114
|
+
for (;;) {
|
|
61115
|
+
const l = 2 * i + 1;
|
|
61116
|
+
const r = 2 * i + 2;
|
|
61117
|
+
let smallest = i;
|
|
61118
|
+
if (l < n && this.prio[l] < this.prio[smallest])
|
|
61119
|
+
smallest = l;
|
|
61120
|
+
if (r < n && this.prio[r] < this.prio[smallest])
|
|
61121
|
+
smallest = r;
|
|
61122
|
+
if (smallest === i)
|
|
61123
|
+
break;
|
|
61124
|
+
this.swap(i, smallest);
|
|
61125
|
+
i = smallest;
|
|
61126
|
+
}
|
|
61127
|
+
}
|
|
61128
|
+
return topId;
|
|
61129
|
+
}
|
|
61130
|
+
swap(a, b) {
|
|
61131
|
+
const ti = this.ids[a];
|
|
61132
|
+
this.ids[a] = this.ids[b];
|
|
61133
|
+
this.ids[b] = ti;
|
|
61134
|
+
const tp = this.prio[a];
|
|
61135
|
+
this.prio[a] = this.prio[b];
|
|
61136
|
+
this.prio[b] = tp;
|
|
61137
|
+
}
|
|
61138
|
+
}
|
|
58857
61139
|
|
|
58858
|
-
|
|
61140
|
+
/**
|
|
61141
|
+
* 模式 B(测地线)状态机:逐次点击锚点,相邻锚点之间用 MeshGraph 求最短路径,
|
|
61142
|
+
* 拼成一条贴合表面的折线。Enter 结束并可闭合(首尾再求一段)。
|
|
61143
|
+
*/
|
|
61144
|
+
class GeodesicContour {
|
|
61145
|
+
constructor(graph, mesh) {
|
|
61146
|
+
this.graph = graph;
|
|
61147
|
+
this.mesh = mesh;
|
|
61148
|
+
this.anchors = []; // 顶点索引
|
|
61149
|
+
this.segments = []; // 相邻锚点间路径(含端点)
|
|
61150
|
+
}
|
|
61151
|
+
/** 传入 local 命中点,snap 到最近顶点并对上一锚点求路径。 */
|
|
61152
|
+
addAnchor(localHitPoint) {
|
|
61153
|
+
const v = this.graph.nearestVertex(localHitPoint);
|
|
61154
|
+
if (this.anchors.length > 0) {
|
|
61155
|
+
const prev = this.anchors[this.anchors.length - 1];
|
|
61156
|
+
this.segments.push(this.graph.shortestPath(prev, v));
|
|
61157
|
+
}
|
|
61158
|
+
this.anchors.push(v);
|
|
61159
|
+
}
|
|
61160
|
+
get anchorCount() {
|
|
61161
|
+
return this.anchors.length;
|
|
61162
|
+
}
|
|
61163
|
+
/** 把所有路径顶点(world)+ 法线 拼成折线;closed 时补一段首尾路径。 */
|
|
61164
|
+
buildVertices(closed) {
|
|
61165
|
+
const segs = this.segments.slice();
|
|
61166
|
+
if (closed && this.anchors.length > 2) {
|
|
61167
|
+
segs.push(this.graph.shortestPath(this.anchors[this.anchors.length - 1], this.anchors[0]));
|
|
61168
|
+
}
|
|
61169
|
+
const idxPath = [];
|
|
61170
|
+
segs.forEach((s, si) => {
|
|
61171
|
+
const start = si === 0 ? 0 : 1; // 去重相邻段共享端点
|
|
61172
|
+
for (let k = start; k < s.length; k++)
|
|
61173
|
+
idxPath.push(s[k]);
|
|
61174
|
+
});
|
|
61175
|
+
if (idxPath.length === 0 && this.anchors.length === 1) {
|
|
61176
|
+
idxPath.push(this.anchors[0]);
|
|
61177
|
+
}
|
|
61178
|
+
// 图几何即 local 空间,直接产出 local 顶点(渲染时再派生 world)。
|
|
61179
|
+
return idxPath.map((i) => this.graph.vertexLocal(i));
|
|
61180
|
+
}
|
|
61181
|
+
}
|
|
58859
61182
|
|
|
58860
|
-
|
|
61183
|
+
/**
|
|
61184
|
+
* 标注数据模型 + 撤销栈 + 导出。纯数据,无 three / DOM 依赖。
|
|
61185
|
+
* 撤销只记录 add / remove 两类原子操作(满足"撤销最近添加 / 恢复删除")。
|
|
61186
|
+
* 通过 subscribe 通知 UI 与渲染层(后者据 list() 做场景对账)。
|
|
61187
|
+
*/
|
|
61188
|
+
class AnnotationStore {
|
|
61189
|
+
constructor() {
|
|
61190
|
+
this.items = [];
|
|
61191
|
+
this.undoStack = [];
|
|
61192
|
+
this.subs = new Set();
|
|
61193
|
+
}
|
|
61194
|
+
notify() {
|
|
61195
|
+
this.subs.forEach((f) => f());
|
|
61196
|
+
}
|
|
61197
|
+
subscribe(cb) {
|
|
61198
|
+
this.subs.add(cb);
|
|
61199
|
+
return () => this.subs.delete(cb);
|
|
61200
|
+
}
|
|
61201
|
+
add(a) {
|
|
61202
|
+
this.items.push(a);
|
|
61203
|
+
this.undoStack.push({ kind: "add", ann: a });
|
|
61204
|
+
this.notify();
|
|
61205
|
+
}
|
|
61206
|
+
remove(id) {
|
|
61207
|
+
const i = this.items.findIndex((x) => x.id === id);
|
|
61208
|
+
if (i < 0)
|
|
61209
|
+
return undefined;
|
|
61210
|
+
const [ann] = this.items.splice(i, 1);
|
|
61211
|
+
this.undoStack.push({ kind: "remove", ann });
|
|
61212
|
+
this.notify();
|
|
61213
|
+
return ann;
|
|
61214
|
+
}
|
|
61215
|
+
undo() {
|
|
61216
|
+
const op = this.undoStack.pop();
|
|
61217
|
+
if (!op)
|
|
61218
|
+
return;
|
|
61219
|
+
if (op.kind === "add") {
|
|
61220
|
+
const i = this.items.findIndex((x) => x.id === op.ann.id);
|
|
61221
|
+
if (i >= 0)
|
|
61222
|
+
this.items.splice(i, 1);
|
|
61223
|
+
}
|
|
61224
|
+
else {
|
|
61225
|
+
this.items.push(op.ann);
|
|
61226
|
+
}
|
|
61227
|
+
this.notify();
|
|
61228
|
+
}
|
|
61229
|
+
/** 清空,返回被清掉的项(供渲染层 dispose three 对象)。 */
|
|
61230
|
+
clear() {
|
|
61231
|
+
const old = this.items;
|
|
61232
|
+
this.items = [];
|
|
61233
|
+
this.undoStack = [];
|
|
61234
|
+
this.notify();
|
|
61235
|
+
return old;
|
|
61236
|
+
}
|
|
61237
|
+
list() {
|
|
61238
|
+
return this.items;
|
|
61239
|
+
}
|
|
61240
|
+
get(id) {
|
|
61241
|
+
return this.items.find((x) => x.id === id);
|
|
61242
|
+
}
|
|
61243
|
+
setLabel(id, label) {
|
|
61244
|
+
const a = this.get(id);
|
|
61245
|
+
if (a) {
|
|
61246
|
+
a.label = label;
|
|
61247
|
+
this.notify();
|
|
61248
|
+
}
|
|
61249
|
+
}
|
|
61250
|
+
setColor(id, color) {
|
|
61251
|
+
const a = this.get(id);
|
|
61252
|
+
if (a) {
|
|
61253
|
+
a.color = color;
|
|
61254
|
+
this.notify();
|
|
61255
|
+
}
|
|
61256
|
+
}
|
|
61257
|
+
toJSON(modelName, mesh, opts = {}) {
|
|
61258
|
+
var _a;
|
|
61259
|
+
const space = (_a = opts.space) !== null && _a !== void 0 ? _a : "local";
|
|
61260
|
+
const toPt = (v) => {
|
|
61261
|
+
let x = v.x, y = v.y, z = v.z;
|
|
61262
|
+
if (space === "world") {
|
|
61263
|
+
const p = new Vector3(v.x, v.y, v.z).applyMatrix4(mesh.matrixWorld);
|
|
61264
|
+
x = p.x;
|
|
61265
|
+
y = p.y;
|
|
61266
|
+
z = p.z;
|
|
61267
|
+
}
|
|
61268
|
+
return opts.includeNormals ? [x, y, z, v.nx, v.ny, v.nz] : [x, y, z];
|
|
61269
|
+
};
|
|
61270
|
+
return {
|
|
61271
|
+
model: modelName,
|
|
61272
|
+
exportedAt: new Date().toISOString(),
|
|
61273
|
+
space,
|
|
61274
|
+
annotations: this.items.map((a) => ({
|
|
61275
|
+
id: a.id,
|
|
61276
|
+
type: a.type,
|
|
61277
|
+
mode: a.mode,
|
|
61278
|
+
label: a.label,
|
|
61279
|
+
color: a.color,
|
|
61280
|
+
closed: a.closed,
|
|
61281
|
+
points: a.vertices.map(toPt),
|
|
61282
|
+
})),
|
|
61283
|
+
};
|
|
61284
|
+
}
|
|
61285
|
+
}
|
|
58861
61286
|
|
|
61287
|
+
const LINE_W = 3;
|
|
61288
|
+
const LINE_W_SEL = 6;
|
|
61289
|
+
/**
|
|
61290
|
+
* 表面标注主控制器(Phase 4):navigate / freehand / geodesic / point 四模式,
|
|
61291
|
+
* Enter 闭合,数据交给 AnnotationStore 管理(多条带颜色标签、撤销/删除/清空、导出)。
|
|
61292
|
+
* 渲染层据 store.list() 做场景对账(reconcile),撤销/删除自动加/移 three 对象。
|
|
61293
|
+
*/
|
|
61294
|
+
class SurfaceAnnotator {
|
|
61295
|
+
constructor(opts) {
|
|
61296
|
+
var _a, _b;
|
|
61297
|
+
this.mode = "navigate";
|
|
61298
|
+
this.spaceHeld = false;
|
|
61299
|
+
this.pointerDown = false;
|
|
61300
|
+
this.store = new AnnotationStore();
|
|
61301
|
+
this.managed = new Set();
|
|
61302
|
+
this.seq = 0;
|
|
61303
|
+
this.selectedId = null;
|
|
61304
|
+
/** 窗口尺寸变化时更新所有 fat line 的像素分辨率,否则线宽会失真。 */
|
|
61305
|
+
this.onResize = () => {
|
|
61306
|
+
const w = this.o.container.clientWidth;
|
|
61307
|
+
const h = this.o.container.clientHeight;
|
|
61308
|
+
for (const a of this.store.list()) {
|
|
61309
|
+
if (a.type === "contour" && a.object3D) {
|
|
61310
|
+
const mat = a.object3D.material;
|
|
61311
|
+
mat.resolution.set(w, h);
|
|
61312
|
+
}
|
|
61313
|
+
}
|
|
61314
|
+
if (this.activeLine)
|
|
61315
|
+
(this.activeLine.material.resolution.set(w, h));
|
|
61316
|
+
if (this.activeGeoLine)
|
|
61317
|
+
(this.activeGeoLine.material.resolution.set(w, h));
|
|
61318
|
+
};
|
|
61319
|
+
this.onPointerDown = (e) => {
|
|
61320
|
+
if (this.spaceHeld)
|
|
61321
|
+
return;
|
|
61322
|
+
if (e.button !== 0)
|
|
61323
|
+
return; // 仅左键
|
|
61324
|
+
if (!this.insideContainer(e))
|
|
61325
|
+
return;
|
|
61326
|
+
this.pointerDown = true;
|
|
61327
|
+
if (this.mode === "point") {
|
|
61328
|
+
const h = this.hit(e);
|
|
61329
|
+
if (!h)
|
|
61330
|
+
return;
|
|
61331
|
+
const v = worldHitToLocalVertex(h, this.o.mesh);
|
|
61332
|
+
const marker = makePointMarker(v, this.o.mesh, this.pointColorVal, this.markerRadius);
|
|
61333
|
+
this.o.scene.add(marker);
|
|
61334
|
+
const ann = {
|
|
61335
|
+
id: this.nextId(),
|
|
61336
|
+
type: "points",
|
|
61337
|
+
mode: null,
|
|
61338
|
+
label: `Point ${this.seq}`,
|
|
61339
|
+
color: this.pointColorVal,
|
|
61340
|
+
closed: false,
|
|
61341
|
+
vertices: [v],
|
|
61342
|
+
object3D: marker,
|
|
61343
|
+
};
|
|
61344
|
+
this.store.add(ann);
|
|
61345
|
+
return;
|
|
61346
|
+
}
|
|
61347
|
+
if (this.mode === "freehand") {
|
|
61348
|
+
this.activeStroke = new StrokeContour(this.minGap, this.o.mesh);
|
|
61349
|
+
this.activeStroke.begin();
|
|
61350
|
+
const h = this.hit(e);
|
|
61351
|
+
if (h)
|
|
61352
|
+
this.activeStroke.addSample(h);
|
|
61353
|
+
this.activeLine = makeContourLine(this.activeStroke.vertices, this.freehandColor, false, this.o.container, this.epsilon, this.o.mesh);
|
|
61354
|
+
this.o.scene.add(this.activeLine);
|
|
61355
|
+
return;
|
|
61356
|
+
}
|
|
61357
|
+
if (this.mode === "geodesic") {
|
|
61358
|
+
const h = this.hit(e);
|
|
61359
|
+
if (!h)
|
|
61360
|
+
return;
|
|
61361
|
+
if (!this.activeGeo) {
|
|
61362
|
+
this.activeGeo = new GeodesicContour(this.graph, this.o.mesh);
|
|
61363
|
+
}
|
|
61364
|
+
const local = this.o.mesh.worldToLocal(h.point.clone());
|
|
61365
|
+
this.activeGeo.addAnchor(local);
|
|
61366
|
+
const verts = this.activeGeo.buildVertices(false);
|
|
61367
|
+
if (!this.activeGeoLine) {
|
|
61368
|
+
this.activeGeoLine = makeContourLine(verts, this.geodesicColor, false, this.o.container, this.epsilon, this.o.mesh);
|
|
61369
|
+
this.o.scene.add(this.activeGeoLine);
|
|
61370
|
+
}
|
|
61371
|
+
else {
|
|
61372
|
+
updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
|
|
61373
|
+
}
|
|
61374
|
+
}
|
|
61375
|
+
};
|
|
61376
|
+
this.onPointerMove = (e) => {
|
|
61377
|
+
if (this.spaceHeld)
|
|
61378
|
+
return;
|
|
61379
|
+
if (this.mode === "freehand" &&
|
|
61380
|
+
this.pointerDown &&
|
|
61381
|
+
this.activeStroke &&
|
|
61382
|
+
this.activeLine) {
|
|
61383
|
+
const h = this.hit(e);
|
|
61384
|
+
if (!h)
|
|
61385
|
+
return;
|
|
61386
|
+
this.activeStroke.addSample(h);
|
|
61387
|
+
updateContourLine(this.activeLine, this.activeStroke.vertices, false, this.epsilon, this.o.mesh);
|
|
61388
|
+
}
|
|
61389
|
+
};
|
|
61390
|
+
this.onPointerUp = () => {
|
|
61391
|
+
if (!this.pointerDown)
|
|
61392
|
+
return;
|
|
61393
|
+
this.pointerDown = false;
|
|
61394
|
+
if (this.activeStroke && this.activeLine) {
|
|
61395
|
+
const verts = this.activeStroke.end();
|
|
61396
|
+
if (verts.length >= 2) {
|
|
61397
|
+
const ann = {
|
|
61398
|
+
id: this.nextId(),
|
|
61399
|
+
type: "contour",
|
|
61400
|
+
mode: "freehand",
|
|
61401
|
+
label: `Contour ${this.seq}`,
|
|
61402
|
+
color: this.freehandColor,
|
|
61403
|
+
closed: false,
|
|
61404
|
+
vertices: verts,
|
|
61405
|
+
object3D: this.activeLine,
|
|
61406
|
+
};
|
|
61407
|
+
this.store.add(ann);
|
|
61408
|
+
this.lastFreehand = ann;
|
|
61409
|
+
}
|
|
61410
|
+
else {
|
|
61411
|
+
this.o.scene.remove(this.activeLine);
|
|
61412
|
+
}
|
|
61413
|
+
this.activeStroke = undefined;
|
|
61414
|
+
this.activeLine = undefined;
|
|
61415
|
+
}
|
|
61416
|
+
};
|
|
61417
|
+
this.onKeyDown = (e) => {
|
|
61418
|
+
if (this.isTypingTarget(e))
|
|
61419
|
+
return;
|
|
61420
|
+
if (e.code === "Space") {
|
|
61421
|
+
this.spaceHeld = true;
|
|
61422
|
+
this.applyCameraGating();
|
|
61423
|
+
return;
|
|
61424
|
+
}
|
|
61425
|
+
if (e.key === "Escape") {
|
|
61426
|
+
this.setMode("navigate");
|
|
61427
|
+
return;
|
|
61428
|
+
}
|
|
61429
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === "z" || e.key === "Z")) {
|
|
61430
|
+
e.preventDefault();
|
|
61431
|
+
this.undo();
|
|
61432
|
+
return;
|
|
61433
|
+
}
|
|
61434
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
61435
|
+
if (this.selectedId)
|
|
61436
|
+
this.deleteAnnotation(this.selectedId);
|
|
61437
|
+
return;
|
|
61438
|
+
}
|
|
61439
|
+
if (e.key === "Enter") {
|
|
61440
|
+
// 测地线进行中 → 结束并闭合;否则闭合最近一条自由手绘线。
|
|
61441
|
+
if (this.activeGeo)
|
|
61442
|
+
this.finishGeodesic();
|
|
61443
|
+
else
|
|
61444
|
+
this.closeLastContour();
|
|
61445
|
+
return;
|
|
61446
|
+
}
|
|
61447
|
+
if (e.key === "1")
|
|
61448
|
+
this.setMode("navigate");
|
|
61449
|
+
if (e.key === "2")
|
|
61450
|
+
this.setMode("freehand");
|
|
61451
|
+
if (e.key === "3")
|
|
61452
|
+
this.setMode("geodesic");
|
|
61453
|
+
if (e.key === "4")
|
|
61454
|
+
this.setMode("point");
|
|
61455
|
+
};
|
|
61456
|
+
this.onKeyUp = (e) => {
|
|
61457
|
+
if (e.code === "Space") {
|
|
61458
|
+
this.spaceHeld = false;
|
|
61459
|
+
this.applyCameraGating();
|
|
61460
|
+
}
|
|
61461
|
+
};
|
|
61462
|
+
this.o = opts;
|
|
61463
|
+
const meshGeo = opts.mesh.geometry;
|
|
61464
|
+
if (!meshGeo.getAttribute("normal"))
|
|
61465
|
+
meshGeo.computeVertexNormals(); // 渲染需要法线
|
|
61466
|
+
meshGeo.computeBoundingBox();
|
|
61467
|
+
const diag = (_a = opts.bboxDiagonal) !== null && _a !== void 0 ? _a : meshGeo.boundingBox.getSize(new Vector3()).length();
|
|
61468
|
+
this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.006;
|
|
61469
|
+
this.epsilon = diag * 0.002;
|
|
61470
|
+
this.minGap = diag * 0.004;
|
|
61471
|
+
// 测地线连通性:单独焊接一份"仅位置"的几何给 MeshGraph 用——
|
|
61472
|
+
// 不动被渲染的 mesh(保留其法线/UV/纹理)。仅位置可保证同表面位置的顶点
|
|
61473
|
+
// 被 mergeVertices 合并(否则逐面法线/UV 会阻止合并,图断裂 → 闭合穿模)。
|
|
61474
|
+
const posOnly = new BufferGeometry();
|
|
61475
|
+
posOnly.setAttribute("position", meshGeo.getAttribute("position").clone());
|
|
61476
|
+
const graphGeo = mergeVertices(posOnly);
|
|
61477
|
+
graphGeo.computeVertexNormals();
|
|
61478
|
+
this.graph = new MeshGraph(graphGeo);
|
|
61479
|
+
this.store.subscribe(() => this.reconcile());
|
|
61480
|
+
// 监听挂到 window 的捕获阶段(capture=true):在 copper 的 TrackballControls
|
|
61481
|
+
// 之前拿到每一个指针事件,避免它的 setPointerCapture / 事件路由导致拖拽中途丢 move。
|
|
61482
|
+
window.addEventListener("pointerdown", this.onPointerDown, true);
|
|
61483
|
+
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
61484
|
+
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
61485
|
+
window.addEventListener("keydown", this.onKeyDown);
|
|
61486
|
+
window.addEventListener("keyup", this.onKeyUp);
|
|
61487
|
+
window.addEventListener("resize", this.onResize);
|
|
61488
|
+
this.applyCameraGating();
|
|
61489
|
+
}
|
|
61490
|
+
get freehandColor() {
|
|
61491
|
+
var _a;
|
|
61492
|
+
return (_a = this.o.freehandColor) !== null && _a !== void 0 ? _a : "#e5006e";
|
|
61493
|
+
}
|
|
61494
|
+
get geodesicColor() {
|
|
61495
|
+
var _a;
|
|
61496
|
+
return (_a = this.o.geodesicColor) !== null && _a !== void 0 ? _a : "#ffa24e";
|
|
61497
|
+
}
|
|
61498
|
+
get pointColorVal() {
|
|
61499
|
+
var _a;
|
|
61500
|
+
return (_a = this.o.pointColor) !== null && _a !== void 0 ? _a : "#ffd166";
|
|
61501
|
+
}
|
|
61502
|
+
// ---- 对外 API(供 Vue 调用) ----
|
|
61503
|
+
getMode() {
|
|
61504
|
+
return this.mode;
|
|
61505
|
+
}
|
|
61506
|
+
setMode(m) {
|
|
61507
|
+
var _a, _b;
|
|
61508
|
+
this.mode = m;
|
|
61509
|
+
this.applyCameraGating();
|
|
61510
|
+
(_b = (_a = this.o).onModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, m);
|
|
61511
|
+
}
|
|
61512
|
+
getStore() {
|
|
61513
|
+
return this.store;
|
|
61514
|
+
}
|
|
61515
|
+
/** 当前标注列表快照。 */
|
|
61516
|
+
getAnnotations() {
|
|
61517
|
+
return this.store.list();
|
|
61518
|
+
}
|
|
61519
|
+
undo() {
|
|
61520
|
+
this.store.undo();
|
|
61521
|
+
}
|
|
61522
|
+
clearAll() {
|
|
61523
|
+
const removed = this.store.clear();
|
|
61524
|
+
removed.forEach((a) => {
|
|
61525
|
+
if (a.object3D) {
|
|
61526
|
+
this.o.scene.remove(a.object3D);
|
|
61527
|
+
this.disposeObject(a.object3D);
|
|
61528
|
+
}
|
|
61529
|
+
});
|
|
61530
|
+
this.managed.clear();
|
|
61531
|
+
this.selectedId = null;
|
|
61532
|
+
}
|
|
61533
|
+
deleteAnnotation(id) {
|
|
61534
|
+
if (this.selectedId === id)
|
|
61535
|
+
this.selectedId = null;
|
|
61536
|
+
this.store.remove(id);
|
|
61537
|
+
}
|
|
61538
|
+
selectAnnotation(id) {
|
|
61539
|
+
this.selectedId = id;
|
|
61540
|
+
this.applySelection();
|
|
61541
|
+
}
|
|
61542
|
+
/** 颜色变更后重画对应 three 对象。 */
|
|
61543
|
+
refreshAnnotation(id) {
|
|
61544
|
+
const a = this.store.get(id);
|
|
61545
|
+
if (!a || !a.object3D)
|
|
61546
|
+
return;
|
|
61547
|
+
if (a.type === "contour") {
|
|
61548
|
+
setContourColor(a.object3D, a.color);
|
|
61549
|
+
}
|
|
61550
|
+
else {
|
|
61551
|
+
const m = a.object3D;
|
|
61552
|
+
m.material.color.set(a.color);
|
|
61553
|
+
}
|
|
61554
|
+
}
|
|
61555
|
+
exportJSON(modelName, opts) {
|
|
61556
|
+
return this.store.toJSON(modelName, this.o.mesh, opts);
|
|
61557
|
+
}
|
|
61558
|
+
// ---- 内部 ----
|
|
61559
|
+
applyCameraGating() {
|
|
61560
|
+
// 按住 Space 临时旋转优先;否则仅 navigate 模式开相机。
|
|
61561
|
+
this.o.controls.enabled = this.spaceHeld || this.mode === "navigate";
|
|
61562
|
+
}
|
|
61563
|
+
/** 据 store.list() 对账场景:补齐缺失对象、移除已删对象(不 dispose,留给撤销恢复)。 */
|
|
61564
|
+
reconcile() {
|
|
61565
|
+
var _a, _b;
|
|
61566
|
+
const present = new Set();
|
|
61567
|
+
for (const a of this.store.list())
|
|
61568
|
+
if (a.object3D)
|
|
61569
|
+
present.add(a.object3D);
|
|
61570
|
+
for (const o of this.managed) {
|
|
61571
|
+
if (!present.has(o))
|
|
61572
|
+
this.o.scene.remove(o);
|
|
61573
|
+
}
|
|
61574
|
+
for (const o of present) {
|
|
61575
|
+
if (!this.managed.has(o))
|
|
61576
|
+
this.o.scene.add(o);
|
|
61577
|
+
}
|
|
61578
|
+
this.managed = present;
|
|
61579
|
+
this.applySelection();
|
|
61580
|
+
(_b = (_a = this.o).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, this.store.list());
|
|
61581
|
+
}
|
|
61582
|
+
applySelection() {
|
|
61583
|
+
for (const a of this.store.list()) {
|
|
61584
|
+
if (!a.object3D)
|
|
61585
|
+
continue;
|
|
61586
|
+
const sel = a.id === this.selectedId;
|
|
61587
|
+
if (a.type === "contour") {
|
|
61588
|
+
const mat = a.object3D.material;
|
|
61589
|
+
mat.linewidth = sel ? LINE_W_SEL : LINE_W;
|
|
61590
|
+
}
|
|
61591
|
+
else {
|
|
61592
|
+
a.object3D.scale.setScalar(sel ? 1.6 : 1);
|
|
61593
|
+
}
|
|
61594
|
+
}
|
|
61595
|
+
}
|
|
61596
|
+
disposeObject(o) {
|
|
61597
|
+
var _a, _b, _c;
|
|
61598
|
+
const any = o;
|
|
61599
|
+
(_b = (_a = any.geometry) === null || _a === void 0 ? void 0 : _a.dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
61600
|
+
const mat = any.material;
|
|
61601
|
+
if (Array.isArray(mat))
|
|
61602
|
+
mat.forEach((m) => m.dispose());
|
|
61603
|
+
else
|
|
61604
|
+
(_c = mat === null || mat === void 0 ? void 0 : mat.dispose) === null || _c === void 0 ? void 0 : _c.call(mat);
|
|
61605
|
+
}
|
|
61606
|
+
nextId() {
|
|
61607
|
+
return "a" + ++this.seq;
|
|
61608
|
+
}
|
|
61609
|
+
hit(e) {
|
|
61610
|
+
return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
|
|
61611
|
+
}
|
|
61612
|
+
/** 事件目标是否落在标注容器内(排除面板/页面其它区域的点击)。 */
|
|
61613
|
+
insideContainer(e) {
|
|
61614
|
+
const t = e.target;
|
|
61615
|
+
return !!t && this.o.container.contains(t);
|
|
61616
|
+
}
|
|
61617
|
+
closeLastContour() {
|
|
61618
|
+
const last = this.lastFreehand;
|
|
61619
|
+
if (!last || last.closed || last.vertices.length < 3 || !last.object3D)
|
|
61620
|
+
return;
|
|
61621
|
+
// 用测地线沿表面把末点连回首点,避免直线弦从模型内部"抄近路"被遮挡。
|
|
61622
|
+
const tail = last.vertices[last.vertices.length - 1];
|
|
61623
|
+
const head = last.vertices[0];
|
|
61624
|
+
const closing = this.surfacePathBetween(tail, head);
|
|
61625
|
+
last.vertices = last.vertices.concat(closing);
|
|
61626
|
+
last.closed = true;
|
|
61627
|
+
updateContourLine(last.object3D, last.vertices, true, this.epsilon, this.o.mesh);
|
|
61628
|
+
}
|
|
61629
|
+
/** 沿网格表面求 a→b 的测地路径(local 顶点),去掉与 a 重合的首点。a/b 已是 local。 */
|
|
61630
|
+
surfacePathBetween(a, b) {
|
|
61631
|
+
const va = this.graph.nearestVertex(new Vector3(a.x, a.y, a.z));
|
|
61632
|
+
const vb = this.graph.nearestVertex(new Vector3(b.x, b.y, b.z));
|
|
61633
|
+
const path = this.graph.shortestPath(va, vb);
|
|
61634
|
+
return path.slice(1).map((i) => this.graph.vertexLocal(i));
|
|
61635
|
+
}
|
|
61636
|
+
/** 结束当前测地线:闭合成环并落定为一条 contour。 */
|
|
61637
|
+
finishGeodesic() {
|
|
61638
|
+
if (!this.activeGeo || !this.activeGeoLine)
|
|
61639
|
+
return;
|
|
61640
|
+
const closed = this.activeGeo.anchorCount > 2;
|
|
61641
|
+
const verts = this.activeGeo.buildVertices(closed);
|
|
61642
|
+
if (verts.length >= 2) {
|
|
61643
|
+
updateContourLine(this.activeGeoLine, verts, closed, this.epsilon, this.o.mesh);
|
|
61644
|
+
const ann = {
|
|
61645
|
+
id: this.nextId(),
|
|
61646
|
+
type: "contour",
|
|
61647
|
+
mode: "geodesic",
|
|
61648
|
+
label: `Contour ${this.seq}`,
|
|
61649
|
+
color: this.geodesicColor,
|
|
61650
|
+
closed,
|
|
61651
|
+
vertices: verts,
|
|
61652
|
+
object3D: this.activeGeoLine,
|
|
61653
|
+
};
|
|
61654
|
+
this.store.add(ann);
|
|
61655
|
+
}
|
|
61656
|
+
else {
|
|
61657
|
+
this.o.scene.remove(this.activeGeoLine);
|
|
61658
|
+
}
|
|
61659
|
+
this.activeGeo = undefined;
|
|
61660
|
+
this.activeGeoLine = undefined;
|
|
61661
|
+
}
|
|
61662
|
+
isTypingTarget(e) {
|
|
61663
|
+
const t = e.target;
|
|
61664
|
+
if (!t)
|
|
61665
|
+
return false;
|
|
61666
|
+
const tag = t.tagName;
|
|
61667
|
+
return tag === "INPUT" || tag === "TEXTAREA" || t.isContentEditable;
|
|
61668
|
+
}
|
|
61669
|
+
dispose() {
|
|
61670
|
+
window.removeEventListener("pointerdown", this.onPointerDown, true);
|
|
61671
|
+
window.removeEventListener("pointermove", this.onPointerMove, true);
|
|
61672
|
+
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
61673
|
+
window.removeEventListener("keydown", this.onKeyDown);
|
|
61674
|
+
window.removeEventListener("keyup", this.onKeyUp);
|
|
61675
|
+
window.removeEventListener("resize", this.onResize);
|
|
61676
|
+
}
|
|
58862
61677
|
}
|
|
58863
61678
|
|
|
58864
61679
|
class copperScene extends baseScene {
|
|
@@ -58870,6 +61685,7 @@ class copperScene extends baseScene {
|
|
|
58870
61685
|
this.mixer = null;
|
|
58871
61686
|
this.playRate = 1.0;
|
|
58872
61687
|
this.modelReady = false;
|
|
61688
|
+
this.surfaceAnnotators = [];
|
|
58873
61689
|
if ((opt === null || opt === void 0 ? void 0 : opt.controls) === "trackball") {
|
|
58874
61690
|
this.controls = new TrackballControls(this.camera, this.renderer.domElement);
|
|
58875
61691
|
}
|
|
@@ -58945,53 +61761,45 @@ class copperScene extends baseScene {
|
|
|
58945
61761
|
console.log("An error happened: ", error);
|
|
58946
61762
|
});
|
|
58947
61763
|
}
|
|
58948
|
-
// loadOBJ(url: string, callback?: (mesh: THREE.Group) => void) {
|
|
58949
|
-
// objLoader.load(
|
|
58950
|
-
// url,
|
|
58951
|
-
// (obj) => {
|
|
58952
|
-
// obj.traverse((child) => {
|
|
58953
|
-
// if ((child as THREE.Mesh).isMesh) {
|
|
58954
|
-
// // (child as THREE.Mesh).material = new THREE.MeshStandardMaterial({
|
|
58955
|
-
// // side: THREE.DoubleSide,
|
|
58956
|
-
// // color: 0xffffff,
|
|
58957
|
-
// // });
|
|
58958
|
-
// // ((child as THREE.Mesh).material as THREE.MeshPhongMaterial).color =
|
|
58959
|
-
// // new THREE.Color(0xffffff);
|
|
58960
|
-
// }
|
|
58961
|
-
// });
|
|
58962
|
-
// const box = new THREE.Box3().setFromObject(obj);
|
|
58963
|
-
// const size = box.getSize(new THREE.Vector3()).length();
|
|
58964
|
-
// const center = box.getCenter(new THREE.Vector3());
|
|
58965
|
-
// this.controls.maxDistance = size * 10;
|
|
58966
|
-
// obj.position.x += obj.position.x - center.x;
|
|
58967
|
-
// obj.position.y += obj.position.y - center.y;
|
|
58968
|
-
// obj.position.z += obj.position.z - center.z;
|
|
58969
|
-
// if (!this.cameraPositionFlag) {
|
|
58970
|
-
// this.camera.position.copy(center);
|
|
58971
|
-
// this.camera.position.x += size / 2.0;
|
|
58972
|
-
// this.camera.position.y += size / 5.0;
|
|
58973
|
-
// this.camera.position.z += size / 2.0;
|
|
58974
|
-
// this.camera.lookAt(center);
|
|
58975
|
-
// this.viewPoint = this.setViewPoint(
|
|
58976
|
-
// this.camera as THREE.PerspectiveCamera,
|
|
58977
|
-
// [center.x, center.y, center.z]
|
|
58978
|
-
// );
|
|
58979
|
-
// }
|
|
58980
|
-
// this.scene.add(obj);
|
|
58981
|
-
// !!callback && callback(obj);
|
|
58982
|
-
// }, // called when loading is in progresses
|
|
58983
|
-
// (xhr: any) => {
|
|
58984
|
-
// console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
|
|
58985
|
-
// },
|
|
58986
|
-
// // called when loading has errors
|
|
58987
|
-
// (error: any) => {
|
|
58988
|
-
// console.log("An error happened");
|
|
58989
|
-
// }
|
|
58990
|
-
// );
|
|
58991
|
-
// }
|
|
58992
61764
|
loadVtk(url) {
|
|
58993
61765
|
copperVtkLoader(url, this.scene, this.content);
|
|
58994
61766
|
}
|
|
61767
|
+
/**
|
|
61768
|
+
* 在给定模型表面创建标注器(画 contour / 放点 / 导出坐标)。
|
|
61769
|
+
* target 可为单个 Mesh,或 Group/Object3D(自动选顶点数最多的 mesh)。
|
|
61770
|
+
* 复用本 scene 的 camera / container / controls;几何非索引时内部自动焊接索引化。
|
|
61771
|
+
*/
|
|
61772
|
+
createSurfaceAnnotator(target, opts) {
|
|
61773
|
+
const mesh = this.pickAnnotatableMesh(target);
|
|
61774
|
+
if (!mesh)
|
|
61775
|
+
throw new Error("createSurfaceAnnotator: no mesh found in target");
|
|
61776
|
+
const annotator = new SurfaceAnnotator(Object.assign({ scene: this.scene, camera: this.camera, container: this.container, controls: this.controls, mesh }, (opts !== null && opts !== void 0 ? opts : {})));
|
|
61777
|
+
this.surfaceAnnotators.push(annotator);
|
|
61778
|
+
return annotator;
|
|
61779
|
+
}
|
|
61780
|
+
pickAnnotatableMesh(target) {
|
|
61781
|
+
if (target.isMesh)
|
|
61782
|
+
return target;
|
|
61783
|
+
let best = null;
|
|
61784
|
+
let bestCount = -1;
|
|
61785
|
+
target.traverse((c) => {
|
|
61786
|
+
var _a, _b;
|
|
61787
|
+
const m = c;
|
|
61788
|
+
if (m.isMesh) {
|
|
61789
|
+
const n = (_b = (_a = m.geometry.getAttribute("position")) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0;
|
|
61790
|
+
if (n > bestCount) {
|
|
61791
|
+
bestCount = n;
|
|
61792
|
+
best = m;
|
|
61793
|
+
}
|
|
61794
|
+
}
|
|
61795
|
+
});
|
|
61796
|
+
return best;
|
|
61797
|
+
}
|
|
61798
|
+
/** 释放本 scene 创建的所有标注器(移除事件监听与标注对象)。 */
|
|
61799
|
+
disposeSurfaceAnnotators() {
|
|
61800
|
+
this.surfaceAnnotators.forEach((a) => a.dispose());
|
|
61801
|
+
this.surfaceAnnotators = [];
|
|
61802
|
+
}
|
|
58995
61803
|
loadVtks(models) {
|
|
58996
61804
|
let count = 0;
|
|
58997
61805
|
let { vtkLoader } = copperMultipleVtk();
|
|
@@ -59351,7 +62159,6 @@ class copperRenderer extends baseRenderer {
|
|
|
59351
62159
|
this.delta = 0;
|
|
59352
62160
|
this.interval = 1 / this.fps;
|
|
59353
62161
|
this.preRenderCallbackFunctions = [];
|
|
59354
|
-
this.running = true;
|
|
59355
62162
|
this.animate = (time) => {
|
|
59356
62163
|
var _a, _b, _c;
|
|
59357
62164
|
if (!this.running)
|
|
@@ -59382,10 +62189,6 @@ class copperRenderer extends baseRenderer {
|
|
|
59382
62189
|
}
|
|
59383
62190
|
};
|
|
59384
62191
|
}
|
|
59385
|
-
// Stop the render loop (call on teardown to release the RAF and let GC reclaim).
|
|
59386
|
-
stop() {
|
|
59387
|
-
this.running = false;
|
|
59388
|
-
}
|
|
59389
62192
|
getSceneByName(name) {
|
|
59390
62193
|
return this.sceneMap[name];
|
|
59391
62194
|
}
|
|
@@ -59910,6 +62713,7 @@ class copperMSceneRenderer {
|
|
|
59910
62713
|
alpha: true,
|
|
59911
62714
|
antialias: true,
|
|
59912
62715
|
});
|
|
62716
|
+
this.running = true;
|
|
59913
62717
|
this.renderSceneInfo = (sceneInfo) => {
|
|
59914
62718
|
const elem = sceneInfo.container;
|
|
59915
62719
|
// get the viewpoint relative position of this element
|
|
@@ -59948,6 +62752,8 @@ class copperMSceneRenderer {
|
|
|
59948
62752
|
}
|
|
59949
62753
|
};
|
|
59950
62754
|
this.animate = () => {
|
|
62755
|
+
if (!this.running)
|
|
62756
|
+
return;
|
|
59951
62757
|
const clearColor = new Color$1("#000");
|
|
59952
62758
|
this.renderer.setScissorTest(false);
|
|
59953
62759
|
this.renderer.setClearColor(clearColor, 0);
|
|
@@ -60013,6 +62819,25 @@ class copperMSceneRenderer {
|
|
|
60013
62819
|
}, undefined, reject);
|
|
60014
62820
|
});
|
|
60015
62821
|
}
|
|
62822
|
+
stop() {
|
|
62823
|
+
this.running = false;
|
|
62824
|
+
}
|
|
62825
|
+
// Full teardown: stop the loop, free GPU resources, release the WebGL context, and
|
|
62826
|
+
// remove the canvas. Call on page unmount to avoid context exhaustion / leaks.
|
|
62827
|
+
dispose() {
|
|
62828
|
+
var _a, _b;
|
|
62829
|
+
this.running = false;
|
|
62830
|
+
try {
|
|
62831
|
+
(_a = this.pmremGenerator) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
62832
|
+
this.renderer.dispose();
|
|
62833
|
+
this.renderer.forceContextLoss();
|
|
62834
|
+
}
|
|
62835
|
+
catch (e) {
|
|
62836
|
+
/* ignore */
|
|
62837
|
+
}
|
|
62838
|
+
(_b = this.canvas) === null || _b === void 0 ? void 0 : _b.remove();
|
|
62839
|
+
this.elems.forEach((el) => el.remove());
|
|
62840
|
+
}
|
|
60016
62841
|
}
|
|
60017
62842
|
|
|
60018
62843
|
/******************************************************************************
|
|
@@ -70432,6 +73257,36 @@ const CHANNEL_HEX_COLORS = {
|
|
|
70432
73257
|
7: '#f97316',
|
|
70433
73258
|
8: '#8b5cf6', // Violet
|
|
70434
73259
|
};
|
|
73260
|
+
/**
|
|
73261
|
+
* AI-Assist channel palette. Identical to the default palette EXCEPT:
|
|
73262
|
+
* - channel 1 = cyan (the AI-Assist accent #5ec8ff), so the 2D AI overlay aligns
|
|
73263
|
+
* with the cyan ai_generated GLB in the 3D panel;
|
|
73264
|
+
* - channel 6 takes the emerald that channel 1 vacated, so cyan isn't duplicated.
|
|
73265
|
+
* Applied ONLY to the AI scratch layer (set per-volume in AiAssistTool.enter) —
|
|
73266
|
+
* the global palette above is untouched, so the clinician mask stays emerald.
|
|
73267
|
+
*/
|
|
73268
|
+
const AI_MASK_CHANNEL_COLORS = {
|
|
73269
|
+
0: { r: 0, g: 0, b: 0, a: 0 },
|
|
73270
|
+
1: { r: 94, g: 200, b: 255, a: 255 },
|
|
73271
|
+
2: { r: 244, g: 63, b: 94, a: 255 },
|
|
73272
|
+
3: { r: 59, g: 130, b: 246, a: 255 },
|
|
73273
|
+
4: { r: 251, g: 191, b: 36, a: 255 },
|
|
73274
|
+
5: { r: 217, g: 70, b: 239, a: 255 },
|
|
73275
|
+
6: { r: 16, g: 185, b: 129, a: 255 },
|
|
73276
|
+
7: { r: 249, g: 115, b: 22, a: 255 },
|
|
73277
|
+
8: { r: 139, g: 92, b: 246, a: 255 }, // Violet
|
|
73278
|
+
};
|
|
73279
|
+
const AI_CHANNEL_HEX_COLORS = {
|
|
73280
|
+
0: '#000000',
|
|
73281
|
+
1: '#5ec8ff',
|
|
73282
|
+
2: '#f43f5e',
|
|
73283
|
+
3: '#3b82f6',
|
|
73284
|
+
4: '#fbbf24',
|
|
73285
|
+
5: '#d946ef',
|
|
73286
|
+
6: '#10b981',
|
|
73287
|
+
7: '#f97316',
|
|
73288
|
+
8: '#8b5cf6', // Violet
|
|
73289
|
+
};
|
|
70435
73290
|
// ── Color Conversion Utilities ──────────────────────────────────────────
|
|
70436
73291
|
/**
|
|
70437
73292
|
* Convert an RGBAColor to a hex string (no alpha), e.g. '#ff0000'.
|
|
@@ -73580,6 +76435,8 @@ class EventRouter {
|
|
|
73580
76435
|
constructor(config) {
|
|
73581
76436
|
// === State ===
|
|
73582
76437
|
this.mode = 'idle';
|
|
76438
|
+
/** Mode to restore after a right-drag pan ends (so pan doesn't drop aiAssist). */
|
|
76439
|
+
this.modeBeforePan = 'idle';
|
|
73583
76440
|
this.guiTool = 'pencil';
|
|
73584
76441
|
this.state = {
|
|
73585
76442
|
shiftHeld: false,
|
|
@@ -73705,7 +76562,20 @@ class EventRouter {
|
|
|
73705
76562
|
* Set the GUI tool (from UI buttons).
|
|
73706
76563
|
*/
|
|
73707
76564
|
setGuiTool(tool) {
|
|
76565
|
+
const prevTool = this.guiTool;
|
|
73708
76566
|
this.guiTool = tool;
|
|
76567
|
+
// AI-assist owns the canvas: left-click/drag = prompt. Taking the
|
|
76568
|
+
// 'aiAssist' mode (not 'idle') is what auto-disables left-drag slice
|
|
76569
|
+
// scrubbing — `isDragSliceActive()` requires mode === 'idle'.
|
|
76570
|
+
if (tool === 'aiAssist') {
|
|
76571
|
+
this.state.crosshairEnabled = false;
|
|
76572
|
+
this.setMode('aiAssist');
|
|
76573
|
+
return;
|
|
76574
|
+
}
|
|
76575
|
+
// Leaving aiAssist → restore idle so slice-drag / wheel resume.
|
|
76576
|
+
if (prevTool === 'aiAssist' && this.mode === 'aiAssist') {
|
|
76577
|
+
this.setMode('idle');
|
|
76578
|
+
}
|
|
73709
76579
|
// When entering any sphere-family tool, keep crosshair if active, otherwise idle
|
|
73710
76580
|
if (SPHERE_TOOLS.has(tool)) {
|
|
73711
76581
|
if (!this.state.crosshairEnabled) {
|
|
@@ -73719,8 +76589,9 @@ class EventRouter {
|
|
|
73719
76589
|
* Blocked when draw or contrast mode is active, or left button is held (mutual exclusion).
|
|
73720
76590
|
*/
|
|
73721
76591
|
toggleCrosshair() {
|
|
73722
|
-
// Allow crosshair in drawing tools
|
|
73723
|
-
|
|
76592
|
+
// Allow crosshair in drawing tools, all sphere-family tools, AND aiAssist
|
|
76593
|
+
// (so the crosshair can be used to debug while AI-assist is active).
|
|
76594
|
+
if (!DRAWING_TOOLS.has(this.guiTool) && !SPHERE_TOOLS.has(this.guiTool) && this.guiTool !== 'aiAssist')
|
|
73724
76595
|
return;
|
|
73725
76596
|
// Block crosshair activation during draw, contrast, or while left button held.
|
|
73726
76597
|
// The leftButtonDown guard also enforces "once a sphere preview is on screen,
|
|
@@ -73728,7 +76599,15 @@ class EventRouter {
|
|
|
73728
76599
|
if (this.state.shiftHeld || this.state.leftButtonDown || this.mode === 'draw' || this.mode === 'contrast')
|
|
73729
76600
|
return;
|
|
73730
76601
|
this.state.crosshairEnabled = !this.state.crosshairEnabled;
|
|
73731
|
-
|
|
76602
|
+
// Crosshair has PRIORITY over aiAssist: turning it on takes the mode (so
|
|
76603
|
+
// AI point/box/scribble are suspended); turning it off restores aiAssist
|
|
76604
|
+
// (not idle) when the AI tool is the active tool, otherwise idle.
|
|
76605
|
+
if (this.state.crosshairEnabled) {
|
|
76606
|
+
this.setMode('crosshair');
|
|
76607
|
+
}
|
|
76608
|
+
else {
|
|
76609
|
+
this.setMode(this.guiTool === 'aiAssist' ? 'aiAssist' : 'idle');
|
|
76610
|
+
}
|
|
73732
76611
|
}
|
|
73733
76612
|
/**
|
|
73734
76613
|
* Check if crosshair mode is enabled.
|
|
@@ -73864,8 +76743,8 @@ class EventRouter {
|
|
|
73864
76743
|
}
|
|
73865
76744
|
}
|
|
73866
76745
|
if (this.contrastEnabled && this.keyboardSettings.contrast.includes(ev.key)) {
|
|
73867
|
-
// Block contrast state when crosshair, draw, or any sphere-family tool is active (mutual exclusion)
|
|
73868
|
-
if (!this.state.crosshairEnabled && this.mode !== 'draw'
|
|
76746
|
+
// Block contrast state when crosshair, draw, aiAssist, or any sphere-family tool is active (mutual exclusion)
|
|
76747
|
+
if (!this.state.crosshairEnabled && this.mode !== 'draw' && this.mode !== 'aiAssist'
|
|
73869
76748
|
&& !SPHERE_TOOLS.has(this.guiTool)) {
|
|
73870
76749
|
this.state.ctrlHeld = true;
|
|
73871
76750
|
}
|
|
@@ -73902,6 +76781,10 @@ class EventRouter {
|
|
|
73902
76781
|
}
|
|
73903
76782
|
else if (ev.button === 2) {
|
|
73904
76783
|
this.state.rightButtonDown = true;
|
|
76784
|
+
// Remember the mode we're panning out of so we can restore it on
|
|
76785
|
+
// release — otherwise aiAssist (and any non-idle mode) is lost.
|
|
76786
|
+
if (this.mode !== 'pan')
|
|
76787
|
+
this.modeBeforePan = this.mode;
|
|
73905
76788
|
this.setMode('pan');
|
|
73906
76789
|
}
|
|
73907
76790
|
// Route to external handler
|
|
@@ -73922,7 +76805,10 @@ class EventRouter {
|
|
|
73922
76805
|
else if (ev.button === 2) {
|
|
73923
76806
|
this.state.rightButtonDown = false;
|
|
73924
76807
|
if (this.mode === 'pan') {
|
|
73925
|
-
|
|
76808
|
+
// Restore aiAssist if we panned out of it; otherwise idle (unchanged
|
|
76809
|
+
// behaviour for normal drawing modes).
|
|
76810
|
+
this.setMode(this.modeBeforePan === 'aiAssist' ? 'aiAssist' : 'idle');
|
|
76811
|
+
this.modeBeforePan = 'idle';
|
|
73926
76812
|
}
|
|
73927
76813
|
}
|
|
73928
76814
|
// Route to external handler
|
|
@@ -75845,6 +78731,474 @@ class SphereBrushTool extends BaseTool {
|
|
|
75845
78731
|
}
|
|
75846
78732
|
}
|
|
75847
78733
|
|
|
78734
|
+
/**
|
|
78735
|
+
* AiAssistTool — interactive prompt-based segmentation (experimental).
|
|
78736
|
+
*
|
|
78737
|
+
* The clinician left-clicks foreground / background points (or drags a box /
|
|
78738
|
+
* scribbles) on the current slice; a backend model returns a region mask that
|
|
78739
|
+
* is written into an INDEPENDENT scratch MaskVolume (`aiAssistMaskVolume`,
|
|
78740
|
+
* registered as `maskData.volumes['aiScratch']`) — never the validated layers.
|
|
78741
|
+
*
|
|
78742
|
+
* Network lives in the app layer: this tool only (a) maps screen → voxel-slice
|
|
78743
|
+
* coordinates, (b) accumulates the prompt for the current slice, (c) fires
|
|
78744
|
+
* `onPrompt` so the app can call the backend, and (d) applies the returned mask
|
|
78745
|
+
* via `applyMask`. The app's `useAiAssist` composable wires (c)→(d).
|
|
78746
|
+
*
|
|
78747
|
+
* Sandbox: snapshot on enter (`snapshot`), then on exit either `discard`
|
|
78748
|
+
* (restore snapshot) or merge is handled by NrrdTools (clone + pushGroup).
|
|
78749
|
+
*
|
|
78750
|
+
* Coordinate mapping supports all three views (axial/sagittal/coronal); it
|
|
78751
|
+
* inverts the per-axis display transform of RenderingUtils.renderSliceToCanvas
|
|
78752
|
+
* (only coronal 'y' is vertically flipped). Merge captures painted voxels from
|
|
78753
|
+
* any view (the scratch is a full 3D volume).
|
|
78754
|
+
*/
|
|
78755
|
+
/** Scratch volume key in maskData.volumes — kept OUT of image.layers so
|
|
78756
|
+
* compositeAllLayers ignores it (we render it ourselves in start()). */
|
|
78757
|
+
const AI_SCRATCH_LAYER = "aiScratch";
|
|
78758
|
+
class AiAssistTool extends BaseTool {
|
|
78759
|
+
constructor(ctx, host) {
|
|
78760
|
+
super(ctx);
|
|
78761
|
+
/** Independent scratch volume (also registered at maskData.volumes['aiScratch']). */
|
|
78762
|
+
this.scratchVol = null;
|
|
78763
|
+
/** Snapshot of the scratch buffer taken on enter (for discard). */
|
|
78764
|
+
this.snapshotData = null;
|
|
78765
|
+
/** Frozen regions ("New region" commits here). Pixels recorded here survive a
|
|
78766
|
+
* later re-prediction — even on the SAME channel — so multiple separate regions
|
|
78767
|
+
* can be drawn without the newest prediction wiping the previous ones. Null =
|
|
78768
|
+
* nothing frozen yet (applySliceRle then behaves as a single live region). */
|
|
78769
|
+
this.committedVol = null;
|
|
78770
|
+
this.promptTool = "point";
|
|
78771
|
+
this.polarity = 1; // 1 = foreground (positive), 0 = background
|
|
78772
|
+
/** Target channel/label (1-8) the AI paints into — the "AI layer" channel. */
|
|
78773
|
+
this.channel = 1;
|
|
78774
|
+
/** Scribble brush radius (px), user-adjustable via slider. */
|
|
78775
|
+
this.scribbleSize = 5;
|
|
78776
|
+
/** Accumulated prompts for the CURRENT slice; reset on slice/axis change. */
|
|
78777
|
+
this.points = [];
|
|
78778
|
+
this.scribble = [];
|
|
78779
|
+
this.activeSlice = -1;
|
|
78780
|
+
// Drag state (box / scribble) — voxel coords for the prompt
|
|
78781
|
+
this.dragging = false;
|
|
78782
|
+
this.dragStart = null;
|
|
78783
|
+
// Screen-space (display px) copies for the LIVE preview drawn each frame.
|
|
78784
|
+
this.dragScreenStart = null;
|
|
78785
|
+
this.dragScreen = null;
|
|
78786
|
+
this.screenScribble = [];
|
|
78787
|
+
// Live cursor position (screen px) for the scribble brush-size preview ring —
|
|
78788
|
+
// updated on every hover move so the ring follows the mouse like the paint brush.
|
|
78789
|
+
this.hoverScreen = null;
|
|
78790
|
+
/** Fired when a prompt gesture completes — app calls backend, then applyMask(). */
|
|
78791
|
+
this.onPrompt = null;
|
|
78792
|
+
this.host = host;
|
|
78793
|
+
}
|
|
78794
|
+
// ── Configuration (driven from the panel via NrrdTools) ────────────────────
|
|
78795
|
+
setPromptTool(tool) {
|
|
78796
|
+
// Switching tool starts a clean gesture — drop any stale points/box/scribble
|
|
78797
|
+
// and drag state so the next click behaves predictably.
|
|
78798
|
+
if (tool !== this.promptTool) {
|
|
78799
|
+
this.resetPrompts();
|
|
78800
|
+
this.dragging = false;
|
|
78801
|
+
this.dragStart = null;
|
|
78802
|
+
}
|
|
78803
|
+
this.promptTool = tool;
|
|
78804
|
+
}
|
|
78805
|
+
setPolarity(label) { this.polarity = label === 0 ? 0 : 1; }
|
|
78806
|
+
setScribbleSize(size) {
|
|
78807
|
+
this.scribbleSize = Math.max(1, Math.min(40, Math.round(size)));
|
|
78808
|
+
}
|
|
78809
|
+
setChannel(channel) {
|
|
78810
|
+
const c = Math.max(1, Math.min(8, Math.round(channel)));
|
|
78811
|
+
// Changing channel starts a NEW region in the new colour — otherwise the
|
|
78812
|
+
// accumulated points would be re-predicted and repainted in the new label,
|
|
78813
|
+
// recolouring the previous channel's region.
|
|
78814
|
+
if (c !== this.channel)
|
|
78815
|
+
this.resetPrompts();
|
|
78816
|
+
this.channel = c;
|
|
78817
|
+
}
|
|
78818
|
+
getChannel() { return this.channel; }
|
|
78819
|
+
getScratchVolume() { return this.scratchVol; }
|
|
78820
|
+
/** Serialize the scratch volume as per-z-slice RLE (only NON-empty slices),
|
|
78821
|
+
* for persisting to ai_generated_nii_LPS on the backend. Binary (any channel →
|
|
78822
|
+
* 1); RLE is alternating run lengths starting with a 0-run — the exact format
|
|
78823
|
+
* the backend's rle_decode expects. Uses getSliceUint8 (the same path the
|
|
78824
|
+
* clinician layer's /api/mask/replace uses) so orientation matches. */
|
|
78825
|
+
getScratchSlices() {
|
|
78826
|
+
if (!this.scratchVol)
|
|
78827
|
+
return null;
|
|
78828
|
+
const { depth } = this.scratchVol.getDimensions();
|
|
78829
|
+
const out = [];
|
|
78830
|
+
let width = 0;
|
|
78831
|
+
let height = 0;
|
|
78832
|
+
for (let z = 0; z < depth; z++) {
|
|
78833
|
+
const { data, width: w, height: h } = this.scratchVol.getSliceUint8(z, "z");
|
|
78834
|
+
width = w;
|
|
78835
|
+
height = h;
|
|
78836
|
+
// RLE-encode (binarized) in one pass; skip fully-empty slices.
|
|
78837
|
+
const runs = [];
|
|
78838
|
+
let value = 0; // always begin with a 0-run (matches backend rle_encode)
|
|
78839
|
+
let count = 0;
|
|
78840
|
+
let any = false;
|
|
78841
|
+
for (let i = 0; i < data.length; i++) {
|
|
78842
|
+
const v = data[i] !== 0 ? 1 : 0;
|
|
78843
|
+
if (v)
|
|
78844
|
+
any = true;
|
|
78845
|
+
if (v === value) {
|
|
78846
|
+
count++;
|
|
78847
|
+
}
|
|
78848
|
+
else {
|
|
78849
|
+
runs.push(count);
|
|
78850
|
+
value = v;
|
|
78851
|
+
count = 1;
|
|
78852
|
+
}
|
|
78853
|
+
}
|
|
78854
|
+
runs.push(count);
|
|
78855
|
+
if (any)
|
|
78856
|
+
out.push({ sliceIndex: z, rle: runs });
|
|
78857
|
+
}
|
|
78858
|
+
return { axis: "z", width, height, slices: out };
|
|
78859
|
+
}
|
|
78860
|
+
// ── Sandbox lifecycle ──────────────────────────────────────────────────────
|
|
78861
|
+
/** Create (or reuse) the scratch volume sized to the layer grid and register
|
|
78862
|
+
* it under maskData.volumes['aiScratch']. Snapshots the empty buffer. */
|
|
78863
|
+
enter() {
|
|
78864
|
+
const volumes = this.ctx.protectedData.maskData.volumes;
|
|
78865
|
+
const baseId = this.ctx.nrrd_states.image.layers[0];
|
|
78866
|
+
const base = volumes[baseId];
|
|
78867
|
+
if (!base)
|
|
78868
|
+
return;
|
|
78869
|
+
const d = base.getDimensions();
|
|
78870
|
+
if (!this.scratchVol ||
|
|
78871
|
+
this.scratchVol.getDimensions().width !== d.width ||
|
|
78872
|
+
this.scratchVol.getDimensions().height !== d.height ||
|
|
78873
|
+
this.scratchVol.getDimensions().depth !== d.depth) {
|
|
78874
|
+
this.scratchVol = new MaskVolume(d.width, d.height, d.depth, 1);
|
|
78875
|
+
}
|
|
78876
|
+
else {
|
|
78877
|
+
this.scratchVol.clear();
|
|
78878
|
+
}
|
|
78879
|
+
volumes[AI_SCRATCH_LAYER] = this.scratchVol;
|
|
78880
|
+
// Paint the AI scratch with the AI-Assist palette (channel 1 = cyan) so the 2D
|
|
78881
|
+
// overlay matches the cyan ai_generated GLB. Scoped to THIS volume only — the
|
|
78882
|
+
// global palette / clinician mask colours are untouched.
|
|
78883
|
+
for (let ch = 1; ch <= 8; ch++) {
|
|
78884
|
+
const c = AI_MASK_CHANNEL_COLORS[ch];
|
|
78885
|
+
if (c)
|
|
78886
|
+
this.scratchVol.setChannelColor(ch, { r: c.r, g: c.g, b: c.b, a: c.a });
|
|
78887
|
+
}
|
|
78888
|
+
this.snapshotData = this.scratchVol.getRawData().slice();
|
|
78889
|
+
this.committedVol = null; // nothing frozen at session start
|
|
78890
|
+
this.resetPrompts();
|
|
78891
|
+
}
|
|
78892
|
+
/** Remove the scratch volume from the registry and drop references. */
|
|
78893
|
+
exit() {
|
|
78894
|
+
const volumes = this.ctx.protectedData.maskData.volumes;
|
|
78895
|
+
delete volumes[AI_SCRATCH_LAYER];
|
|
78896
|
+
this.scratchVol = null;
|
|
78897
|
+
this.snapshotData = null;
|
|
78898
|
+
this.committedVol = null;
|
|
78899
|
+
this.resetPrompts();
|
|
78900
|
+
this.dragging = false;
|
|
78901
|
+
this.dragStart = null;
|
|
78902
|
+
}
|
|
78903
|
+
/** Discard everything painted since enter() — restore the snapshot. */
|
|
78904
|
+
discard() {
|
|
78905
|
+
if (this.scratchVol && this.snapshotData) {
|
|
78906
|
+
this.scratchVol.setRawData(this.snapshotData.slice());
|
|
78907
|
+
}
|
|
78908
|
+
this.committedVol = null; // discard wipes frozen regions too
|
|
78909
|
+
this.resetPrompts();
|
|
78910
|
+
}
|
|
78911
|
+
/** "New region": freeze every voxel painted so far so it survives later
|
|
78912
|
+
* re-predictions (even on the same channel), then start a fresh prompt set.
|
|
78913
|
+
* This is what makes drawing multiple separate regions actually work — without
|
|
78914
|
+
* it the next prediction's same-channel cleanup erases the previous region. */
|
|
78915
|
+
commitRegion() {
|
|
78916
|
+
if (this.scratchVol) {
|
|
78917
|
+
const d = this.scratchVol.getDimensions();
|
|
78918
|
+
if (!this.committedVol ||
|
|
78919
|
+
this.committedVol.getDimensions().width !== d.width ||
|
|
78920
|
+
this.committedVol.getDimensions().height !== d.height ||
|
|
78921
|
+
this.committedVol.getDimensions().depth !== d.depth) {
|
|
78922
|
+
this.committedVol = new MaskVolume(d.width, d.height, d.depth, 1);
|
|
78923
|
+
}
|
|
78924
|
+
this.committedVol.setRawData(this.scratchVol.getRawData().slice());
|
|
78925
|
+
}
|
|
78926
|
+
this.resetPrompts();
|
|
78927
|
+
}
|
|
78928
|
+
/** Clear the in-progress prompt set (e.g. slice/axis change). Does NOT freeze —
|
|
78929
|
+
* use commitRegion() for the "New region" action. */
|
|
78930
|
+
resetPrompts() {
|
|
78931
|
+
this.points = [];
|
|
78932
|
+
this.scribble = [];
|
|
78933
|
+
this.box = undefined;
|
|
78934
|
+
this.dragScreenStart = null;
|
|
78935
|
+
this.dragScreen = null;
|
|
78936
|
+
this.screenScribble = [];
|
|
78937
|
+
}
|
|
78938
|
+
// ── Coordinate mapping (all three views) ───────────────────────────────────
|
|
78939
|
+
//
|
|
78940
|
+
// Mapping must INVERT exactly what RenderingUtils.renderSliceToCanvas does to
|
|
78941
|
+
// display the overlay (it is the renderer used by renderOverlay):
|
|
78942
|
+
// - axial (z) / sagittal (x): no flip
|
|
78943
|
+
// - coronal (y): vertical flip (scale(1,-1))
|
|
78944
|
+
// Slice voxel dims & canvas mm extents per axis (matches MaskVolume
|
|
78945
|
+
// getSliceDimensions + SphereTool.setSphereCanvasSize):
|
|
78946
|
+
// z → (W=width, H=height) over (x_mm, y_mm)
|
|
78947
|
+
// y → (W=width, H=depth ) over (x_mm, z_mm) [vertical flip]
|
|
78948
|
+
// x → (W=depth, H=height) over (z_mm, y_mm)
|
|
78949
|
+
/** Voxel-slice (width,height) for the current axis. */
|
|
78950
|
+
sliceDims() {
|
|
78951
|
+
if (!this.scratchVol)
|
|
78952
|
+
return null;
|
|
78953
|
+
const d = this.scratchVol.getDimensions();
|
|
78954
|
+
switch (this.ctx.protectedData.axis) {
|
|
78955
|
+
case "z": return { w: d.width, h: d.height };
|
|
78956
|
+
case "y": return { w: d.width, h: d.depth };
|
|
78957
|
+
case "x": return { w: d.depth, h: d.height };
|
|
78958
|
+
default: return null;
|
|
78959
|
+
}
|
|
78960
|
+
}
|
|
78961
|
+
/** Screen offset → voxel-slice (vx,vy) for the current axis. */
|
|
78962
|
+
toVoxel(e) {
|
|
78963
|
+
const sd = this.sliceDims();
|
|
78964
|
+
if (!sd || !this.scratchVol)
|
|
78965
|
+
return null;
|
|
78966
|
+
const nrrd = this.ctx.nrrd_states;
|
|
78967
|
+
const img = nrrd.image;
|
|
78968
|
+
const mx = e.offsetX / nrrd.view.sizeFactor;
|
|
78969
|
+
const my = e.offsetY / nrrd.view.sizeFactor;
|
|
78970
|
+
let hMM, vMM, flipV;
|
|
78971
|
+
switch (this.ctx.protectedData.axis) {
|
|
78972
|
+
case "z":
|
|
78973
|
+
hMM = img.nrrd_x_mm;
|
|
78974
|
+
vMM = img.nrrd_y_mm;
|
|
78975
|
+
flipV = false;
|
|
78976
|
+
break;
|
|
78977
|
+
case "y":
|
|
78978
|
+
hMM = img.nrrd_x_mm;
|
|
78979
|
+
vMM = img.nrrd_z_mm;
|
|
78980
|
+
flipV = true;
|
|
78981
|
+
break;
|
|
78982
|
+
case "x":
|
|
78983
|
+
hMM = img.nrrd_z_mm;
|
|
78984
|
+
vMM = img.nrrd_y_mm;
|
|
78985
|
+
flipV = false;
|
|
78986
|
+
break;
|
|
78987
|
+
default: return null;
|
|
78988
|
+
}
|
|
78989
|
+
const vx = Math.round(mx * sd.w / hMM);
|
|
78990
|
+
let vy = Math.round(my * sd.h / vMM);
|
|
78991
|
+
if (flipV)
|
|
78992
|
+
vy = sd.h - 1 - vy;
|
|
78993
|
+
if (vx < 0 || vx >= sd.w || vy < 0 || vy >= sd.h)
|
|
78994
|
+
return null;
|
|
78995
|
+
return { vx, vy, w: sd.w, h: sd.h };
|
|
78996
|
+
}
|
|
78997
|
+
// ── Pointer handlers (left button; right stays pan in EventRouter) ──────────
|
|
78998
|
+
onPointerDown(e) {
|
|
78999
|
+
const hit = this.toVoxel(e);
|
|
79000
|
+
if (!hit)
|
|
79001
|
+
return;
|
|
79002
|
+
this.syncSlice();
|
|
79003
|
+
if (this.promptTool === "point") {
|
|
79004
|
+
this.points.push({ x: hit.vx, y: hit.vy, label: this.polarity });
|
|
79005
|
+
this.emit();
|
|
79006
|
+
}
|
|
79007
|
+
else {
|
|
79008
|
+
// box / scribble: begin a drag
|
|
79009
|
+
this.dragging = true;
|
|
79010
|
+
this.dragStart = { x: hit.vx, y: hit.vy };
|
|
79011
|
+
this.dragScreenStart = { x: e.offsetX, y: e.offsetY };
|
|
79012
|
+
this.dragScreen = { x: e.offsetX, y: e.offsetY };
|
|
79013
|
+
this.screenScribble = [];
|
|
79014
|
+
if (this.promptTool === "scribble") {
|
|
79015
|
+
this.scribble.push({ x: hit.vx, y: hit.vy, label: this.polarity });
|
|
79016
|
+
this.screenScribble.push({ x: e.offsetX, y: e.offsetY });
|
|
79017
|
+
}
|
|
79018
|
+
}
|
|
79019
|
+
}
|
|
79020
|
+
onPointerMove(e) {
|
|
79021
|
+
// Track hover for the scribble preview ring even when not drawing.
|
|
79022
|
+
this.hoverScreen = { x: e.offsetX, y: e.offsetY };
|
|
79023
|
+
if (!this.dragging)
|
|
79024
|
+
return;
|
|
79025
|
+
// Track screen pos first so the live preview follows even slightly out of bounds.
|
|
79026
|
+
this.dragScreen = { x: e.offsetX, y: e.offsetY };
|
|
79027
|
+
const hit = this.toVoxel(e);
|
|
79028
|
+
if (!hit)
|
|
79029
|
+
return;
|
|
79030
|
+
if (this.promptTool === "scribble") {
|
|
79031
|
+
this.scribble.push({ x: hit.vx, y: hit.vy, label: this.polarity });
|
|
79032
|
+
this.screenScribble.push({ x: e.offsetX, y: e.offsetY });
|
|
79033
|
+
}
|
|
79034
|
+
else if (this.promptTool === "box" && this.dragStart) {
|
|
79035
|
+
this.box = {
|
|
79036
|
+
x0: this.dragStart.x, y0: this.dragStart.y,
|
|
79037
|
+
x1: hit.vx, y1: hit.vy,
|
|
79038
|
+
label: this.polarity, // foreground box grows within; background box excludes
|
|
79039
|
+
};
|
|
79040
|
+
}
|
|
79041
|
+
}
|
|
79042
|
+
onPointerUp(_e) {
|
|
79043
|
+
if (!this.dragging)
|
|
79044
|
+
return;
|
|
79045
|
+
this.dragging = false;
|
|
79046
|
+
this.emit();
|
|
79047
|
+
this.dragStart = null;
|
|
79048
|
+
this.dragScreenStart = null;
|
|
79049
|
+
this.dragScreen = null;
|
|
79050
|
+
this.screenScribble = [];
|
|
79051
|
+
}
|
|
79052
|
+
/** Reset the accumulated prompt set when the user scrubs to a new slice. */
|
|
79053
|
+
syncSlice() {
|
|
79054
|
+
const slice = this.ctx.nrrd_states.view.currentSliceIndex;
|
|
79055
|
+
if (slice !== this.activeSlice) {
|
|
79056
|
+
this.activeSlice = slice;
|
|
79057
|
+
this.resetPrompts();
|
|
79058
|
+
}
|
|
79059
|
+
}
|
|
79060
|
+
emit() {
|
|
79061
|
+
if (!this.onPrompt)
|
|
79062
|
+
return;
|
|
79063
|
+
const sd = this.sliceDims();
|
|
79064
|
+
if (!sd)
|
|
79065
|
+
return;
|
|
79066
|
+
this.onPrompt({
|
|
79067
|
+
axis: this.ctx.protectedData.axis,
|
|
79068
|
+
sliceIndex: this.ctx.nrrd_states.view.currentSliceIndex,
|
|
79069
|
+
width: sd.w,
|
|
79070
|
+
height: sd.h,
|
|
79071
|
+
points: [...this.points],
|
|
79072
|
+
box: this.box,
|
|
79073
|
+
scribble: this.scribble.length ? [...this.scribble] : undefined,
|
|
79074
|
+
scribbleRadius: this.scribbleSize,
|
|
79075
|
+
});
|
|
79076
|
+
}
|
|
79077
|
+
// ── Apply backend result → scratch volume ──────────────────────────────────
|
|
79078
|
+
applyMask(result) {
|
|
79079
|
+
if (!this.scratchVol)
|
|
79080
|
+
return;
|
|
79081
|
+
// 3D result (engine B): write each slice across the covered span.
|
|
79082
|
+
if (result.slices && result.sliceRange) {
|
|
79083
|
+
const [lo] = result.sliceRange;
|
|
79084
|
+
for (let k = 0; k < result.slices.length; k++) {
|
|
79085
|
+
this.applySliceRle(result.slices[k], result.axis, lo + k, result.width, result.height);
|
|
79086
|
+
}
|
|
79087
|
+
return;
|
|
79088
|
+
}
|
|
79089
|
+
// 2D result (engines mock / regiongrow / medsam2d): single slice.
|
|
79090
|
+
this.applySliceRle(result.rle, result.axis, result.sliceIndex, result.width, result.height);
|
|
79091
|
+
}
|
|
79092
|
+
/** Merge one RLE-encoded predicted slice into the scratch volume at sliceIndex.
|
|
79093
|
+
* Predicted pixels → current channel; stale current-channel pixels cleared
|
|
79094
|
+
* UNLESS they belong to a frozen (committed) region; OTHER channels preserved. */
|
|
79095
|
+
applySliceRle(rle, axis, sliceIndex, width, height) {
|
|
79096
|
+
if (!this.scratchVol)
|
|
79097
|
+
return;
|
|
79098
|
+
const predicted = new Uint8Array(width * height);
|
|
79099
|
+
let pos = 0;
|
|
79100
|
+
let value = 0;
|
|
79101
|
+
for (const run of rle) {
|
|
79102
|
+
if (value === 1 && run > 0) {
|
|
79103
|
+
const end = Math.min(pos + run, predicted.length);
|
|
79104
|
+
for (let i = pos; i < end; i++)
|
|
79105
|
+
predicted[i] = 1;
|
|
79106
|
+
}
|
|
79107
|
+
pos += run;
|
|
79108
|
+
value ^= 1;
|
|
79109
|
+
}
|
|
79110
|
+
const ch = this.channel;
|
|
79111
|
+
try {
|
|
79112
|
+
const out = this.scratchVol.getSliceUint8(sliceIndex, axis).data; // copy
|
|
79113
|
+
// Frozen same-channel pixels for this slice (if any region was committed).
|
|
79114
|
+
let frozen = null;
|
|
79115
|
+
if (this.committedVol) {
|
|
79116
|
+
try {
|
|
79117
|
+
frozen = this.committedVol.getSliceUint8(sliceIndex, axis).data;
|
|
79118
|
+
}
|
|
79119
|
+
catch (_a) {
|
|
79120
|
+
frozen = null;
|
|
79121
|
+
}
|
|
79122
|
+
}
|
|
79123
|
+
const n = Math.min(out.length, predicted.length);
|
|
79124
|
+
for (let i = 0; i < n; i++) {
|
|
79125
|
+
if (predicted[i])
|
|
79126
|
+
out[i] = ch;
|
|
79127
|
+
// Clear stale LIVE same-channel pixels (refinement), but never erase a
|
|
79128
|
+
// frozen region committed via "New region".
|
|
79129
|
+
else if (out[i] === ch && !(frozen && frozen[i] === ch))
|
|
79130
|
+
out[i] = 0;
|
|
79131
|
+
}
|
|
79132
|
+
this.scratchVol.setSliceUint8(sliceIndex, out, axis);
|
|
79133
|
+
}
|
|
79134
|
+
catch (_b) {
|
|
79135
|
+
// slice out of bounds / dims changed — ignore
|
|
79136
|
+
}
|
|
79137
|
+
}
|
|
79138
|
+
// ── Overlay render (called from DrawToolCore.start() while in aiAssist) ──────
|
|
79139
|
+
renderOverlay(targetCtx) {
|
|
79140
|
+
if (!this.scratchVol)
|
|
79141
|
+
return;
|
|
79142
|
+
const axis = this.ctx.protectedData.axis;
|
|
79143
|
+
const slice = this.ctx.nrrd_states.view.currentSliceIndex;
|
|
79144
|
+
const buffer = this.host.getOrCreateSliceBuffer(axis);
|
|
79145
|
+
if (!buffer)
|
|
79146
|
+
return;
|
|
79147
|
+
try {
|
|
79148
|
+
this.host.renderSliceToCanvas(AI_SCRATCH_LAYER, axis, slice, buffer, targetCtx, this.ctx.nrrd_states.view.changedWidth, this.ctx.nrrd_states.view.changedHeight);
|
|
79149
|
+
}
|
|
79150
|
+
catch (_a) {
|
|
79151
|
+
// volume not ready / slice out of bounds
|
|
79152
|
+
}
|
|
79153
|
+
// Live drag preview (screen space) — rubber-band box / scribble stroke so the
|
|
79154
|
+
// user sees the gesture before the prediction lands on pointer-up.
|
|
79155
|
+
if (this.dragging && this.dragScreenStart && this.dragScreen) {
|
|
79156
|
+
targetCtx.save();
|
|
79157
|
+
// Green-ish for foreground (include), red-ish for background (exclude).
|
|
79158
|
+
const color = this.polarity === 1 ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79159
|
+
targetCtx.strokeStyle = color;
|
|
79160
|
+
if (this.promptTool === "box") {
|
|
79161
|
+
targetCtx.setLineDash([5, 4]);
|
|
79162
|
+
targetCtx.lineWidth = 1.5;
|
|
79163
|
+
const x = Math.min(this.dragScreenStart.x, this.dragScreen.x);
|
|
79164
|
+
const y = Math.min(this.dragScreenStart.y, this.dragScreen.y);
|
|
79165
|
+
const w = Math.abs(this.dragScreen.x - this.dragScreenStart.x);
|
|
79166
|
+
const h = Math.abs(this.dragScreen.y - this.dragScreenStart.y);
|
|
79167
|
+
targetCtx.strokeRect(x, y, w, h);
|
|
79168
|
+
}
|
|
79169
|
+
else if (this.promptTool === "scribble" && this.screenScribble.length > 1) {
|
|
79170
|
+
targetCtx.lineCap = "round";
|
|
79171
|
+
targetCtx.lineJoin = "round";
|
|
79172
|
+
// Stroke thickness reflects the scribble brush size (diameter ≈ 2·radius).
|
|
79173
|
+
targetCtx.lineWidth = Math.max(2, this.scribbleSize * 2);
|
|
79174
|
+
targetCtx.beginPath();
|
|
79175
|
+
targetCtx.moveTo(this.screenScribble[0].x, this.screenScribble[0].y);
|
|
79176
|
+
for (let i = 1; i < this.screenScribble.length; i++) {
|
|
79177
|
+
targetCtx.lineTo(this.screenScribble[i].x, this.screenScribble[i].y);
|
|
79178
|
+
}
|
|
79179
|
+
targetCtx.stroke();
|
|
79180
|
+
}
|
|
79181
|
+
targetCtx.restore();
|
|
79182
|
+
}
|
|
79183
|
+
// Scribble brush-size preview ring (when NOT dragging) — a circle centred on
|
|
79184
|
+
// the cursor whose radius = scribbleSize, so the user sees the brush size and
|
|
79185
|
+
// how the slider changes it (mirrors the paint brush's preview ring). Drawn
|
|
79186
|
+
// every frame from the live hover position by copper3d's render loop.
|
|
79187
|
+
if (this.promptTool === "scribble" && !this.dragging && this.hoverScreen) {
|
|
79188
|
+
targetCtx.save();
|
|
79189
|
+
const fg = this.polarity === 1;
|
|
79190
|
+
targetCtx.strokeStyle = fg ? "rgba(120,255,180,0.9)" : "rgba(255,120,120,0.9)";
|
|
79191
|
+
targetCtx.lineWidth = 1.5;
|
|
79192
|
+
if (!fg)
|
|
79193
|
+
targetCtx.setLineDash([4, 4]); // dashed for background (exclude), like the eraser
|
|
79194
|
+
targetCtx.beginPath();
|
|
79195
|
+
targetCtx.arc(this.hoverScreen.x, this.hoverScreen.y, Math.max(2, this.scribbleSize), 0, Math.PI * 2);
|
|
79196
|
+
targetCtx.stroke();
|
|
79197
|
+
targetCtx.restore();
|
|
79198
|
+
}
|
|
79199
|
+
}
|
|
79200
|
+
}
|
|
79201
|
+
|
|
75848
79202
|
/**
|
|
75849
79203
|
* DrawToolCore — Tool orchestration and event routing.
|
|
75850
79204
|
*
|
|
@@ -75965,6 +79319,10 @@ class DrawToolCore {
|
|
|
75965
79319
|
setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
|
|
75966
79320
|
reloadMasksFromVolume: () => this.reloadMasksFromVolume(),
|
|
75967
79321
|
});
|
|
79322
|
+
this.aiAssistTool = new AiAssistTool(toolCtx, {
|
|
79323
|
+
renderSliceToCanvas: (layer, axis, sliceIndex, buffer, ctx, w, h) => this.renderer.renderSliceToCanvas(layer, axis, sliceIndex, buffer, ctx, w, h),
|
|
79324
|
+
getOrCreateSliceBuffer: (axis) => this.renderer.getOrCreateSliceBuffer(axis),
|
|
79325
|
+
});
|
|
75968
79326
|
}
|
|
75969
79327
|
initDrawToolCore() {
|
|
75970
79328
|
// Initialize EventRouter for centralized event handling
|
|
@@ -76071,6 +79429,8 @@ class DrawToolCore {
|
|
|
76071
79429
|
// Block contrast toggle during crosshair, draw, or sphere (mutual exclusion)
|
|
76072
79430
|
if (this.eventRouter.isCrosshairEnabled() || this.eventRouter.getMode() === 'draw')
|
|
76073
79431
|
return;
|
|
79432
|
+
if (this.eventRouter.getMode() === 'aiAssist')
|
|
79433
|
+
return;
|
|
76074
79434
|
if (this.state.gui_states.mode.sphere)
|
|
76075
79435
|
return;
|
|
76076
79436
|
// Toggle contrast mode manually since it's on keyup
|
|
@@ -76084,6 +79444,10 @@ class DrawToolCore {
|
|
|
76084
79444
|
});
|
|
76085
79445
|
// Register pointer handlers with EventRouter
|
|
76086
79446
|
this.eventRouter.setPointerMoveHandler((e) => {
|
|
79447
|
+
if (this.eventRouter.getMode() === 'aiAssist') {
|
|
79448
|
+
this.aiAssistTool.onPointerMove(e);
|
|
79449
|
+
return;
|
|
79450
|
+
}
|
|
76087
79451
|
if (this.drawingTool.isActive || this.panTool.isActive) {
|
|
76088
79452
|
this.drawingPrameters.handleOnDrawingMouseMove(e);
|
|
76089
79453
|
}
|
|
@@ -76098,6 +79462,14 @@ class DrawToolCore {
|
|
|
76098
79462
|
}
|
|
76099
79463
|
});
|
|
76100
79464
|
this.eventRouter.setPointerUpHandler((e) => {
|
|
79465
|
+
if (this.eventRouter.getMode() === 'aiAssist') {
|
|
79466
|
+
if (e.button === 0)
|
|
79467
|
+
this.aiAssistTool.onPointerUp(e);
|
|
79468
|
+
// Right-button pan must still clean up (cursor/state) even in AI mode.
|
|
79469
|
+
else if (e.button === 2)
|
|
79470
|
+
this.panTool.onPointerUp(e, this.state.gui_states.viewConfig.defaultPaintCursor);
|
|
79471
|
+
return;
|
|
79472
|
+
}
|
|
76101
79473
|
// Restore Scroll:Zoom if we swapped to Scroll:Slice on drag-start.
|
|
76102
79474
|
// Must run outside the drawing-tool gate below: slice-drag doesn't
|
|
76103
79475
|
// flip any of those flags, so the gate would skip restoration.
|
|
@@ -76257,7 +79629,10 @@ class DrawToolCore {
|
|
|
76257
79629
|
this.activeWheelMode = 'none';
|
|
76258
79630
|
}
|
|
76259
79631
|
if (e.button === 0) {
|
|
76260
|
-
if (this.eventRouter.getMode() === '
|
|
79632
|
+
if (this.eventRouter.getMode() === 'aiAssist') {
|
|
79633
|
+
this.aiAssistTool.onPointerDown(e);
|
|
79634
|
+
}
|
|
79635
|
+
else if (this.eventRouter.getMode() === 'draw') {
|
|
76261
79636
|
this.drawingTool.onPointerDown(e);
|
|
76262
79637
|
}
|
|
76263
79638
|
else if (this.eventRouter.isCrosshairEnabled()) {
|
|
@@ -76372,6 +79747,12 @@ class DrawToolCore {
|
|
|
76372
79747
|
|| this.state.gui_states.mode.sphereEraser) {
|
|
76373
79748
|
this.state.protectedData.ctxes.drawingCtx.drawImage(this.state.protectedData.canvases.drawingSphereCanvas, 0, 0, this.state.nrrd_states.view.changedWidth, this.state.nrrd_states.view.changedHeight);
|
|
76374
79749
|
}
|
|
79750
|
+
// AI-assist scratch overlay — drawn each frame whenever an AI session is
|
|
79751
|
+
// active (scratch volume exists), so it stays visible even when crosshair
|
|
79752
|
+
// temporarily takes over the interaction mode for debugging.
|
|
79753
|
+
if (this.aiAssistTool.getScratchVolume()) {
|
|
79754
|
+
this.aiAssistTool.renderOverlay(this.state.protectedData.ctxes.drawingCtx);
|
|
79755
|
+
}
|
|
76375
79756
|
}
|
|
76376
79757
|
else {
|
|
76377
79758
|
this.redrawDisplayCanvas();
|
|
@@ -77976,6 +81357,13 @@ class NrrdTools {
|
|
|
77976
81357
|
this._calculatorActive = false;
|
|
77977
81358
|
/** Stored closure callbacks from gui.ts setupGui() */
|
|
77978
81359
|
this.guiCallbacks = null;
|
|
81360
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81361
|
+
// 10b. AI Assist (experimental) — interactive prompt segmentation
|
|
81362
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81363
|
+
/** Whether AI-assist mode is currently active. */
|
|
81364
|
+
this._aiAssistActive = false;
|
|
81365
|
+
/** Layer visibility snapshot taken on enter, restored on exit. */
|
|
81366
|
+
this._aiPrevVisibility = null;
|
|
77979
81367
|
this.container = container;
|
|
77980
81368
|
// Create shared state
|
|
77981
81369
|
const mainAreaContainer = document.createElement("div");
|
|
@@ -78143,6 +81531,13 @@ class NrrdTools {
|
|
|
78143
81531
|
var _a, _b, _c;
|
|
78144
81532
|
if (!this.guiCallbacks)
|
|
78145
81533
|
return;
|
|
81534
|
+
// While AI-assist owns the canvas, BLOCK switching to other tools — the user
|
|
81535
|
+
// must explicitly Exit AI Assist from its panel first. (The Operation panel
|
|
81536
|
+
// also disables its tool buttons via the AiAssist:ActiveChanged event, so this
|
|
81537
|
+
// is just a defensive guard.)
|
|
81538
|
+
if (this._aiAssistActive && mode !== "aiAssist") {
|
|
81539
|
+
return;
|
|
81540
|
+
}
|
|
78146
81541
|
const prevSphere = this.state.gui_states.mode.sphere;
|
|
78147
81542
|
const prevSphereBrush = this.state.gui_states.mode.sphereBrush;
|
|
78148
81543
|
const prevSphereEraser = this.state.gui_states.mode.sphereEraser;
|
|
@@ -78791,6 +82186,111 @@ class NrrdTools {
|
|
|
78791
82186
|
this.resetLayerCanvas();
|
|
78792
82187
|
this.reloadMasksFromVolume();
|
|
78793
82188
|
}
|
|
82189
|
+
isAiAssistActive() { return this._aiAssistActive; }
|
|
82190
|
+
/**
|
|
82191
|
+
* Enter AI-assist mode (sandbox): hides the existing layer masks so ONLY the
|
|
82192
|
+
* AI overlay is shown, takes canvas ownership (left-click = prompt), and creates
|
|
82193
|
+
* the scratch volume. Right-drag still pans; wheel/slider still scrub slices.
|
|
82194
|
+
* The hidden masks are restored on exit (merge writes into them first if asked).
|
|
82195
|
+
*/
|
|
82196
|
+
enterAiAssistMode() {
|
|
82197
|
+
var _a;
|
|
82198
|
+
if (this._aiAssistActive)
|
|
82199
|
+
return;
|
|
82200
|
+
this._aiAssistActive = true;
|
|
82201
|
+
this.dragOperator.removeDragMode();
|
|
82202
|
+
// Hide all layer masks (visibility=false survives recomposite on slice change).
|
|
82203
|
+
const vis = this.state.gui_states.layerChannel.layerVisibility;
|
|
82204
|
+
this._aiPrevVisibility = Object.assign({}, vis);
|
|
82205
|
+
for (const layerId of this.state.nrrd_states.image.layers)
|
|
82206
|
+
vis[layerId] = false;
|
|
82207
|
+
this.drawCore.renderer.compositeAllLayers();
|
|
82208
|
+
this.drawCore.aiAssistTool.enter();
|
|
82209
|
+
(_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool("aiAssist");
|
|
82210
|
+
}
|
|
82211
|
+
/**
|
|
82212
|
+
* Exit AI-assist mode: drop the scratch volume, restore normal tooling AND the
|
|
82213
|
+
* layer-mask visibility hidden on enter. If the caller merged first, the merged
|
|
82214
|
+
* result is now part of the layer volume and reappears with the restored masks.
|
|
82215
|
+
*/
|
|
82216
|
+
exitAiAssistMode() {
|
|
82217
|
+
var _a;
|
|
82218
|
+
if (!this._aiAssistActive)
|
|
82219
|
+
return;
|
|
82220
|
+
this._aiAssistActive = false;
|
|
82221
|
+
this.drawCore.aiAssistTool.exit();
|
|
82222
|
+
this.dragOperator.configDragMode();
|
|
82223
|
+
(_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool("pencil");
|
|
82224
|
+
if (this._aiPrevVisibility) {
|
|
82225
|
+
const vis = this.state.gui_states.layerChannel.layerVisibility;
|
|
82226
|
+
for (const k of Object.keys(this._aiPrevVisibility))
|
|
82227
|
+
vis[k] = this._aiPrevVisibility[k];
|
|
82228
|
+
this._aiPrevVisibility = null;
|
|
82229
|
+
}
|
|
82230
|
+
this.reloadMasksFromVolume();
|
|
82231
|
+
}
|
|
82232
|
+
// — Driver methods called by the app-layer composable —
|
|
82233
|
+
aiSetPromptTool(tool) { this.drawCore.aiAssistTool.setPromptTool(tool); }
|
|
82234
|
+
aiSetPolarity(label) { this.drawCore.aiAssistTool.setPolarity(label); }
|
|
82235
|
+
/** Set the AI-layer channel (1-8) the predictions paint into. */
|
|
82236
|
+
aiSetChannel(channel) { this.drawCore.aiAssistTool.setChannel(channel); }
|
|
82237
|
+
/** Set the scribble brush radius (px). */
|
|
82238
|
+
aiSetScribbleSize(size) { this.drawCore.aiAssistTool.setScribbleSize(size); }
|
|
82239
|
+
/** Register the callback invoked when a prompt gesture completes (app → backend). */
|
|
82240
|
+
aiOnPrompt(cb) { this.drawCore.aiAssistTool.onPrompt = cb; }
|
|
82241
|
+
/** Apply a backend mask result into the scratch volume (overlay repaints next frame). */
|
|
82242
|
+
aiApplyMask(result) { this.drawCore.aiAssistTool.applyMask(result); }
|
|
82243
|
+
/** Clear the in-progress prompt set (e.g. slice change). */
|
|
82244
|
+
aiClearPrompts() { this.drawCore.aiAssistTool.resetPrompts(); }
|
|
82245
|
+
/** "New region": freeze current regions (they persist) + start a fresh prompt set. */
|
|
82246
|
+
aiCommitRegion() { this.drawCore.aiAssistTool.commitRegion(); }
|
|
82247
|
+
/** Discard all AI scratch painting since enter (sandbox discard). */
|
|
82248
|
+
aiDiscard() { this.drawCore.aiAssistTool.discard(); }
|
|
82249
|
+
/** True if the scratch volume holds any predicted voxels. */
|
|
82250
|
+
aiHasData() { var _a, _b; return (_b = (_a = this.drawCore.aiAssistTool.getScratchVolume()) === null || _a === void 0 ? void 0 : _a.hasData()) !== null && _b !== void 0 ? _b : false; }
|
|
82251
|
+
/** Serialize the AI scratch as per-slice RLE for persisting to ai_generated_nii_LPS. */
|
|
82252
|
+
aiGetScratchSlices() {
|
|
82253
|
+
return this.drawCore.aiAssistTool.getScratchSlices();
|
|
82254
|
+
}
|
|
82255
|
+
/**
|
|
82256
|
+
* Merge the AI scratch into a target layer as a single undoable group (sandbox
|
|
82257
|
+
* merge — best-practice: non-destructive + one Ctrl+Z). The scratch's channel
|
|
82258
|
+
* labels are PRESERVED (each AI channel maps onto the same channel of the
|
|
82259
|
+
* target layer). Scans all z-slices, so voxels painted from any view are caught.
|
|
82260
|
+
*/
|
|
82261
|
+
aiCommitToLayer(targetLayer = "layer1") {
|
|
82262
|
+
const scratch = this.drawCore.aiAssistTool.getScratchVolume();
|
|
82263
|
+
if (!scratch)
|
|
82264
|
+
return;
|
|
82265
|
+
const target = this.state.protectedData.maskData.volumes[targetLayer];
|
|
82266
|
+
if (!target)
|
|
82267
|
+
return;
|
|
82268
|
+
const dims = scratch.getDimensions();
|
|
82269
|
+
const deltas = [];
|
|
82270
|
+
for (let z = 0; z < dims.depth; z++) {
|
|
82271
|
+
const sc = scratch.getSliceUint8(z, "z").data;
|
|
82272
|
+
let any = false;
|
|
82273
|
+
for (let i = 0; i < sc.length; i++) {
|
|
82274
|
+
if (sc[i] !== 0) {
|
|
82275
|
+
any = true;
|
|
82276
|
+
break;
|
|
82277
|
+
}
|
|
82278
|
+
}
|
|
82279
|
+
if (!any)
|
|
82280
|
+
continue;
|
|
82281
|
+
const oldSlice = target.getSliceUint8(z, "z").data; // copy
|
|
82282
|
+
const newSlice = oldSlice.slice();
|
|
82283
|
+
for (let i = 0; i < newSlice.length; i++) {
|
|
82284
|
+
if (sc[i] !== 0)
|
|
82285
|
+
newSlice[i] = sc[i]; // preserve the AI channel label
|
|
82286
|
+
}
|
|
82287
|
+
target.setSliceUint8(z, newSlice, "z");
|
|
82288
|
+
deltas.push({ layerId: targetLayer, axis: "z", sliceIndex: z, oldSlice, newSlice: newSlice.slice() });
|
|
82289
|
+
}
|
|
82290
|
+
if (deltas.length)
|
|
82291
|
+
this.drawCore.undoManager.pushGroup(deltas);
|
|
82292
|
+
this.reloadMasksFromVolume();
|
|
82293
|
+
}
|
|
78794
82294
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
78795
82295
|
// 11. Clear / Reset
|
|
78796
82296
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -79154,7 +82654,7 @@ function evaluateElement(element, weights) {
|
|
|
79154
82654
|
}
|
|
79155
82655
|
|
|
79156
82656
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
79157
|
-
const REVISION = "v3.
|
|
82657
|
+
const REVISION = "v3.6.0-beta";
|
|
79158
82658
|
console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
|
|
79159
82659
|
|
|
79160
|
-
export { CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };
|
|
82660
|
+
export { AI_CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, SurfaceAnnotator, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };
|