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