copper3d 3.6.2 → 3.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Loader/copperNrrdLoader.js +1 -1
- package/dist/Loader/copperNrrdLoader.js.map +1 -1
- package/dist/Scene/copperScene.d.ts +4 -4
- package/dist/Scene/copperScene.js +4 -4
- package/dist/Scene/copperScene.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +41 -2
- package/dist/Utils/segmentation/NrrdTools.js +30 -2
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/tools/AiAssistTool.d.ts +108 -5
- package/dist/Utils/segmentation/tools/AiAssistTool.js +408 -25
- package/dist/Utils/segmentation/tools/AiAssistTool.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/MeshGraph.d.ts +12 -10
- package/dist/Utils/surfaceAnnotation/MeshGraph.js +14 -12
- package/dist/Utils/surfaceAnnotation/MeshGraph.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.d.ts +48 -28
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.js +76 -50
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/annotationStore.d.ts +4 -4
- package/dist/Utils/surfaceAnnotation/annotationStore.js +4 -4
- package/dist/Utils/surfaceAnnotation/annotationStore.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/contourRender.d.ts +1 -1
- package/dist/Utils/surfaceAnnotation/contourRender.js +16 -11
- package/dist/Utils/surfaceAnnotation/contourRender.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/geodesicContour.d.ts +11 -10
- package/dist/Utils/surfaceAnnotation/geodesicContour.js +16 -15
- package/dist/Utils/surfaceAnnotation/geodesicContour.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/pointMarkers.d.ts +3 -2
- package/dist/Utils/surfaceAnnotation/pointMarkers.js +6 -2
- package/dist/Utils/surfaceAnnotation/pointMarkers.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/raycastSurface.d.ts +3 -2
- package/dist/Utils/surfaceAnnotation/raycastSurface.js +71 -5
- package/dist/Utils/surfaceAnnotation/raycastSurface.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/strokeContour.d.ts +9 -6
- package/dist/Utils/surfaceAnnotation/strokeContour.js +15 -10
- package/dist/Utils/surfaceAnnotation/strokeContour.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/types.d.ts +5 -5
- package/dist/Utils/surfaceAnnotation/types.js +2 -2
- package/dist/Utils/surfaceAnnotation/types.js.map +1 -1
- package/dist/Utils/texture2d.js +4 -2
- package/dist/Utils/texture2d.js.map +1 -1
- package/dist/Utils/workers/reformatSaveDataWorker.js +2 -2
- package/dist/Utils/workers/reformatSaveDataWorker.js.map +1 -1
- package/dist/bundle.esm.js +684 -157
- package/dist/bundle.umd.js +684 -157
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/three-vignette.js +8 -2
- package/dist/lib/three-vignette.js.map +1 -1
- package/dist/types/Scene/copperScene.d.ts +4 -4
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +41 -2
- package/dist/types/Utils/segmentation/tools/AiAssistTool.d.ts +108 -5
- package/dist/types/Utils/surfaceAnnotation/MeshGraph.d.ts +12 -10
- package/dist/types/Utils/surfaceAnnotation/SurfaceAnnotator.d.ts +48 -28
- package/dist/types/Utils/surfaceAnnotation/annotationStore.d.ts +4 -4
- package/dist/types/Utils/surfaceAnnotation/contourRender.d.ts +1 -1
- package/dist/types/Utils/surfaceAnnotation/geodesicContour.d.ts +11 -10
- package/dist/types/Utils/surfaceAnnotation/pointMarkers.d.ts +3 -2
- package/dist/types/Utils/surfaceAnnotation/raycastSurface.d.ts +3 -2
- package/dist/types/Utils/surfaceAnnotation/strokeContour.d.ts +9 -6
- package/dist/types/Utils/surfaceAnnotation/types.d.ts +5 -5
- package/dist/types/index.d.ts +1 -1
- package/package.json +2 -1
package/dist/bundle.esm.js
CHANGED
|
@@ -40865,9 +40865,9 @@ class CameraViewPoint {
|
|
|
40865
40865
|
}
|
|
40866
40866
|
}
|
|
40867
40867
|
|
|
40868
|
-
var vert = "
|
|
40868
|
+
var vert = "attribute vec3 position;\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nvarying vec2 vUv;\nvoid main() {\n gl_Position = vec4(position, 1.0);\n vUv = vec2(position.x, position.y) * 0.5 + 0.5;\n}\n";
|
|
40869
40869
|
|
|
40870
|
-
var frag = "precision mediump float;\n#
|
|
40870
|
+
var frag = "precision mediump float;\n#pragma glslify: grain = require('glsl-film-grain')\n#pragma glslify: blend = require('glsl-blend-soft-light')\n\nuniform vec3 color1;\nuniform vec3 color2;\nuniform float aspect;\nuniform vec2 offset;\nuniform vec2 scale;\nuniform float noiseAlpha;\nuniform bool aspectCorrection;\nuniform float grainScale;\nuniform float grainTime;\nuniform vec2 smooth;\n\nvarying vec2 vUv;\n\nvoid main() {\n vec2 q = vec2(vUv - 0.5);\n if (aspectCorrection) {\n q.x *= aspect;\n }\n q /= scale;\n q -= offset;\n float dst = length(q);\n dst = smoothstep(smooth.x, smooth.y, dst);\n vec3 color = mix(color1, color2, dst);\n\n if (noiseAlpha > 0.0 && grainScale > 0.0) {\n float gSize = 1.0 / grainScale;\n float g = grain(vUv, vec2(gSize * aspect, gSize), grainTime);\n vec3 noiseColor = blend(color, vec3(g));\n gl_FragColor.rgb = mix(color, noiseColor, noiseAlpha);\n } else {\n gl_FragColor.rgb = color;\n }\n gl_FragColor.a = 1.0;\n}\n";
|
|
40871
40871
|
|
|
40872
40872
|
function createBackground(opt) {
|
|
40873
40873
|
opt = opt || {};
|
|
@@ -41395,9 +41395,9 @@ function getLut(data, windowWidth, windowCenter, invert, voiLUT) {
|
|
|
41395
41395
|
};
|
|
41396
41396
|
}
|
|
41397
41397
|
|
|
41398
|
-
var vert_2d = "
|
|
41398
|
+
var vert_2d = "out vec2 vUv;\n\nvoid main() {\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n // Use the geometry's own UVs; DICOM rows are top-down so flip v.\n vUv = vec2( uv.x, 1.0 - uv.y );\n\n}\n";
|
|
41399
41399
|
|
|
41400
|
-
var frag_2d = "precision highp float
|
|
41400
|
+
var frag_2d = "precision highp float;\nprecision highp int;\nprecision highp sampler2DArray;\n\nuniform sampler2DArray diffuse;\nin vec2 vUv;\nuniform int depth;\nuniform float uOpacity;\n\nout vec4 outColor;\n\nvoid main() {\n\n vec4 color = texture( diffuse, vec3( vUv, depth ) );\n\n // lighten a bit\n outColor = vec4( color.rrr * 1.5, uOpacity );\n\n}";
|
|
41401
41401
|
|
|
41402
41402
|
let planeWidth = 80;
|
|
41403
41403
|
let planeHeight = 80;
|
|
@@ -46328,7 +46328,7 @@ function copperNrrdTexture3dLoader(url, scene, container, callback) {
|
|
|
46328
46328
|
mesh.position.y += mesh.position.y - center.y;
|
|
46329
46329
|
mesh.position.z += mesh.position.z - center.z;
|
|
46330
46330
|
const boundingBox = new Box3().setFromObject(mesh);
|
|
46331
|
-
//
|
|
46331
|
+
// Get the size of the bounding box
|
|
46332
46332
|
const size_ = new Vector3();
|
|
46333
46333
|
boundingBox.getSize(size_);
|
|
46334
46334
|
scene.add(mesh);
|
|
@@ -58678,7 +58678,7 @@ const _endEvent = { type: 'end' };
|
|
|
58678
58678
|
const _EPS = 0.000001;
|
|
58679
58679
|
const _STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
|
|
58680
58680
|
|
|
58681
|
-
const _v2 = new Vector2();
|
|
58681
|
+
const _v2$1 = new Vector2();
|
|
58682
58682
|
const _mouseChange = new Vector2();
|
|
58683
58683
|
const _objectUp = new Vector3();
|
|
58684
58684
|
const _pan = new Vector3();
|
|
@@ -59074,23 +59074,23 @@ class TrackballControls extends Controls$1 {
|
|
|
59074
59074
|
|
|
59075
59075
|
_getMouseOnScreen( pageX, pageY ) {
|
|
59076
59076
|
|
|
59077
|
-
_v2.set(
|
|
59077
|
+
_v2$1.set(
|
|
59078
59078
|
( pageX - this.screen.left ) / this.screen.width,
|
|
59079
59079
|
( pageY - this.screen.top ) / this.screen.height
|
|
59080
59080
|
);
|
|
59081
59081
|
|
|
59082
|
-
return _v2;
|
|
59082
|
+
return _v2$1;
|
|
59083
59083
|
|
|
59084
59084
|
}
|
|
59085
59085
|
|
|
59086
59086
|
_getMouseOnCircle( pageX, pageY ) {
|
|
59087
59087
|
|
|
59088
|
-
_v2.set(
|
|
59088
|
+
_v2$1.set(
|
|
59089
59089
|
( ( pageX - this.screen.width * 0.5 - this.screen.left ) / ( this.screen.width * 0.5 ) ),
|
|
59090
59090
|
( ( this.screen.height + 2 * ( this.screen.top - pageY ) ) / this.screen.width ) // screen.width intentional
|
|
59091
59091
|
);
|
|
59092
59092
|
|
|
59093
|
-
return _v2;
|
|
59093
|
+
return _v2$1;
|
|
59094
59094
|
|
|
59095
59095
|
}
|
|
59096
59096
|
|
|
@@ -59511,7 +59511,7 @@ function onTouchEnd( event ) {
|
|
|
59511
59511
|
}
|
|
59512
59512
|
|
|
59513
59513
|
const _inv = new Matrix4();
|
|
59514
|
-
/**
|
|
59514
|
+
/** World-space hit point → local vertex (position via worldToLocal, normal via inverse transform). */
|
|
59515
59515
|
function worldHitToLocalVertex(h, mesh) {
|
|
59516
59516
|
const lp = mesh.worldToLocal(h.point.clone());
|
|
59517
59517
|
_inv.copy(mesh.matrixWorld).invert();
|
|
@@ -59526,20 +59526,80 @@ function worldHitToLocalVertex(h, mesh) {
|
|
|
59526
59526
|
faceIndex: h.faceIndex,
|
|
59527
59527
|
};
|
|
59528
59528
|
}
|
|
59529
|
-
const _nm = new Matrix3();
|
|
59530
|
-
/**
|
|
59529
|
+
const _nm$1 = new Matrix3();
|
|
59530
|
+
/** Local vertex → world position + world normal (for rendering). */
|
|
59531
59531
|
function localVertexToWorld(v, mesh) {
|
|
59532
59532
|
const p = new Vector3(v.x, v.y, v.z).applyMatrix4(mesh.matrixWorld);
|
|
59533
|
-
_nm.getNormalMatrix(mesh.matrixWorld);
|
|
59534
|
-
const n = new Vector3(v.nx, v.ny, v.nz).applyMatrix3(_nm).normalize();
|
|
59533
|
+
_nm$1.getNormalMatrix(mesh.matrixWorld);
|
|
59534
|
+
const n = new Vector3(v.nx, v.ny, v.nz).applyMatrix3(_nm$1).normalize();
|
|
59535
59535
|
return { p, n };
|
|
59536
59536
|
}
|
|
59537
59537
|
|
|
59538
59538
|
const ray = new Raycaster();
|
|
59539
59539
|
const ndc = new Vector2();
|
|
59540
|
+
// scratch objects for the smooth-normal interpolation (avoid per-call allocation)
|
|
59541
|
+
const _nm = new Matrix3();
|
|
59542
|
+
const _lp = new Vector3();
|
|
59543
|
+
const _pa = new Vector3();
|
|
59544
|
+
const _pb = new Vector3();
|
|
59545
|
+
const _pc = new Vector3();
|
|
59546
|
+
const _na = new Vector3();
|
|
59547
|
+
const _nb = new Vector3();
|
|
59548
|
+
const _nc = new Vector3();
|
|
59549
|
+
const _v0 = new Vector3();
|
|
59550
|
+
const _v1 = new Vector3();
|
|
59551
|
+
const _v2 = new Vector3();
|
|
59552
|
+
/**
|
|
59553
|
+
* Interpolate a "smooth per-vertex normal" across the hit triangle using barycentric coordinates,
|
|
59554
|
+
* instead of using the per-face normal.
|
|
59555
|
+
*
|
|
59556
|
+
* Per-face normals on voxel / marching-cubes meshes are axis-aligned stair directions (±x/±y/±z);
|
|
59557
|
+
* using them to push the annotation line outward along the normal shoves points toward neighboring
|
|
59558
|
+
* stairs rather than cleanly "outward", making the line dip in and out on bumpy surfaces and get
|
|
59559
|
+
* occluded by the steps. A smooth normal varies continuously along the surface, so the outward
|
|
59560
|
+
* direction is stable and consistent and the line can float smoothly above the relief. Returns false
|
|
59561
|
+
* when interpolation isn't possible (caller falls back to the face normal).
|
|
59562
|
+
*/
|
|
59563
|
+
function interpolateLocalNormal(geom, face, localPoint, out) {
|
|
59564
|
+
const posAttr = geom.getAttribute("position");
|
|
59565
|
+
const norAttr = geom.getAttribute("normal");
|
|
59566
|
+
if (!posAttr || !norAttr)
|
|
59567
|
+
return false;
|
|
59568
|
+
_pa.fromBufferAttribute(posAttr, face.a);
|
|
59569
|
+
_pb.fromBufferAttribute(posAttr, face.b);
|
|
59570
|
+
_pc.fromBufferAttribute(posAttr, face.c);
|
|
59571
|
+
// Barycentric coordinates (local space): u/v/w
|
|
59572
|
+
_v0.subVectors(_pb, _pa);
|
|
59573
|
+
_v1.subVectors(_pc, _pa);
|
|
59574
|
+
_v2.subVectors(localPoint, _pa);
|
|
59575
|
+
const d00 = _v0.dot(_v0);
|
|
59576
|
+
const d01 = _v0.dot(_v1);
|
|
59577
|
+
const d11 = _v1.dot(_v1);
|
|
59578
|
+
const d20 = _v2.dot(_v0);
|
|
59579
|
+
const d21 = _v2.dot(_v1);
|
|
59580
|
+
const denom = d00 * d11 - d01 * d01;
|
|
59581
|
+
if (Math.abs(denom) < 1e-12)
|
|
59582
|
+
return false;
|
|
59583
|
+
const v = (d11 * d20 - d01 * d21) / denom;
|
|
59584
|
+
const w = (d00 * d21 - d01 * d20) / denom;
|
|
59585
|
+
const u = 1 - v - w;
|
|
59586
|
+
_na.fromBufferAttribute(norAttr, face.a);
|
|
59587
|
+
_nb.fromBufferAttribute(norAttr, face.b);
|
|
59588
|
+
_nc.fromBufferAttribute(norAttr, face.c);
|
|
59589
|
+
out
|
|
59590
|
+
.set(0, 0, 0)
|
|
59591
|
+
.addScaledVector(_na, u)
|
|
59592
|
+
.addScaledVector(_nb, v)
|
|
59593
|
+
.addScaledVector(_nc, w);
|
|
59594
|
+
if (out.lengthSq() < 1e-12)
|
|
59595
|
+
return false;
|
|
59596
|
+
out.normalize();
|
|
59597
|
+
return true;
|
|
59598
|
+
}
|
|
59540
59599
|
/**
|
|
59541
|
-
*
|
|
59542
|
-
*
|
|
59600
|
+
* Project screen coordinates (clientX/Y) onto the mesh surface, returning the hit point,
|
|
59601
|
+
* world-space normal, and faceIndex. Returns null on a miss. Local implementation that does not
|
|
59602
|
+
* depend on Copper3D's unexported raycast internals.
|
|
59543
59603
|
*/
|
|
59544
59604
|
function raycastSurface(camera, container, mesh, clientX, clientY) {
|
|
59545
59605
|
var _a;
|
|
@@ -59554,19 +59614,29 @@ function raycastSurface(camera, container, mesh, clientX, clientY) {
|
|
|
59554
59614
|
const point = h.point.clone();
|
|
59555
59615
|
const normal = new Vector3(0, 0, 1);
|
|
59556
59616
|
if (h.face) {
|
|
59557
|
-
normal
|
|
59558
|
-
|
|
59559
|
-
|
|
59617
|
+
// Prefer a smooth interpolated normal (stable outward direction on bumpy/voxel meshes);
|
|
59618
|
+
// fall back to the flat face normal if the geometry has no normal attribute.
|
|
59619
|
+
_lp.copy(h.point);
|
|
59620
|
+
mesh.worldToLocal(_lp);
|
|
59621
|
+
if (!interpolateLocalNormal(mesh.geometry, h.face, _lp, normal)) {
|
|
59622
|
+
normal.copy(h.face.normal);
|
|
59623
|
+
}
|
|
59624
|
+
_nm.getNormalMatrix(mesh.matrixWorld);
|
|
59625
|
+
normal.applyMatrix3(_nm).normalize();
|
|
59560
59626
|
}
|
|
59561
59627
|
return { point, normal, faceIndex: (_a = h.faceIndex) !== null && _a !== void 0 ? _a : -1 };
|
|
59562
59628
|
}
|
|
59563
59629
|
|
|
59564
59630
|
/**
|
|
59565
|
-
*
|
|
59566
|
-
*
|
|
59631
|
+
* Create a marker sphere (fiducial) at a local vertex. After local→world, nudge it
|
|
59632
|
+
* outward along the world normal so it doesn't sink halfway into the surface.
|
|
59633
|
+
* radius is passed in by the caller, scaled to the model bbox.
|
|
59567
59634
|
*/
|
|
59568
59635
|
function makePointMarker(v, mesh, color, radius) {
|
|
59569
59636
|
const geo = new SphereGeometry(radius, 16, 16);
|
|
59637
|
+
// depthTest ON so markers are occluded by the model when they are on the far side (no
|
|
59638
|
+
// see-through), consistent with the contour lines. Markers are volumetric spheres lifted off
|
|
59639
|
+
// the surface, so they still show on the visible side without z-fighting.
|
|
59570
59640
|
const mat = new MeshBasicMaterial({ color });
|
|
59571
59641
|
const m = new Mesh(geo, mat);
|
|
59572
59642
|
const { p, n } = localVertexToWorld(v, mesh);
|
|
@@ -59576,15 +59646,18 @@ function makePointMarker(v, mesh, color, radius) {
|
|
|
59576
59646
|
}
|
|
59577
59647
|
|
|
59578
59648
|
/**
|
|
59579
|
-
*
|
|
59580
|
-
* pointermove
|
|
59581
|
-
*
|
|
59649
|
+
* State machine for one stroke in mode A (freehand).
|
|
59650
|
+
* On pointermove it keeps calling addSample; samples whose world distance from the previous
|
|
59651
|
+
* sample is < minGap are dropped to avoid over-density.
|
|
59652
|
+
* Vertices are stored in local space (the mesh converts the world hit point to local).
|
|
59582
59653
|
*/
|
|
59583
59654
|
class StrokeContour {
|
|
59584
59655
|
/**
|
|
59585
|
-
* @param minGap
|
|
59586
|
-
* @param maxJump
|
|
59587
|
-
*
|
|
59656
|
+
* @param minGap minimum sample spacing (world space), debounce
|
|
59657
|
+
* @param maxJump jump threshold (world space): drop the sample when distance > maxJump and the
|
|
59658
|
+
* normal flips sharply, to prevent adjacent points landing at different depths
|
|
59659
|
+
* (and the straight segment cutting through the model) when the stroke grazes a
|
|
59660
|
+
* groove or contour edge.
|
|
59588
59661
|
*/
|
|
59589
59662
|
constructor(minGap, maxJump, mesh) {
|
|
59590
59663
|
this.minGap = minGap;
|
|
@@ -59603,10 +59676,12 @@ class StrokeContour {
|
|
|
59603
59676
|
if (this.has) {
|
|
59604
59677
|
const d = hit.point.distanceTo(this.last);
|
|
59605
59678
|
if (d < this.minGap)
|
|
59606
|
-
return; //
|
|
59607
|
-
//
|
|
59608
|
-
//
|
|
59609
|
-
//
|
|
59679
|
+
return; // too close, debounce
|
|
59680
|
+
// Jump rejection: distance suddenly large AND normal flips sharply (>~75°) → likely jumped to
|
|
59681
|
+
// the back/far side, so drop it, otherwise the straight segment between adjacent points cuts
|
|
59682
|
+
// through the model interior. Only reject when both conditions hold:
|
|
59683
|
+
// fast drawing (large distance, similar normals) isn't falsely dropped, and drawing over a
|
|
59684
|
+
// sharp edge (normal changes, distance small) isn't falsely dropped either.
|
|
59610
59685
|
if (d > this.maxJump && hit.normal.dot(this.lastNormal) < 0.25)
|
|
59611
59686
|
return;
|
|
59612
59687
|
}
|
|
@@ -60904,9 +60979,10 @@ class Line2 extends LineSegments2 {
|
|
|
60904
60979
|
}
|
|
60905
60980
|
|
|
60906
60981
|
/**
|
|
60907
|
-
*
|
|
60908
|
-
*
|
|
60909
|
-
*
|
|
60982
|
+
* Flatten a sequence of annotation vertices (local) into the world [x,y,z, ...] that Line2 needs.
|
|
60983
|
+
* Each point goes local→world, then is pushed outward along the world normal by epsilon to avoid
|
|
60984
|
+
* z-fighting with the model surface.
|
|
60985
|
+
* When closed, the first point is appended to the end to form a ring.
|
|
60910
60986
|
*/
|
|
60911
60987
|
function flatten(verts, closed, epsilon, mesh) {
|
|
60912
60988
|
const pts = [];
|
|
@@ -60922,27 +60998,31 @@ function flatten(verts, closed, epsilon, mesh) {
|
|
|
60922
60998
|
function makeContourLine(verts, color, closed, container, epsilon, mesh) {
|
|
60923
60999
|
const geo = new LineGeometry();
|
|
60924
61000
|
const pos = flatten(verts, closed, epsilon, mesh);
|
|
60925
|
-
// Line2
|
|
61001
|
+
// Line2 needs at least 2 points; when there aren't enough, give a degenerate placeholder that a later update fills in.
|
|
60926
61002
|
geo.setPositions(pos.length >= 6 ? pos : [0, 0, 0, 0, 0, 0]);
|
|
60927
61003
|
const mat = new LineMaterial({
|
|
60928
61004
|
color: new Color$1(color).getHex(),
|
|
60929
|
-
linewidth: 3, //
|
|
61005
|
+
linewidth: 3, // width in pixels (requires resolution to be set)
|
|
61006
|
+
// depthTest stays ON so the model correctly occludes the line when it is on the far side
|
|
61007
|
+
// (no see-through). The host applies polygonOffset to the SURFACE material so the line still
|
|
61008
|
+
// wins where it lies coplanar with the surface (no z-fighting / sinking on bumpy meshes).
|
|
60930
61009
|
});
|
|
60931
61010
|
mat.resolution.set(container.clientWidth, container.clientHeight);
|
|
60932
61011
|
const line = new Line2(geo, mat);
|
|
60933
61012
|
line.computeLineDistances();
|
|
60934
61013
|
line.renderOrder = 998;
|
|
60935
|
-
//
|
|
60936
|
-
//
|
|
61014
|
+
// Important: Line2's bounding sphere stays at the initial point, so once the geometry grows by
|
|
61015
|
+
// dragging, the real geometry gets wrongly culled. Disable frustum culling so the line always renders.
|
|
60937
61016
|
line.frustumCulled = false;
|
|
60938
61017
|
return line;
|
|
60939
61018
|
}
|
|
60940
61019
|
function updateContourLine(line, verts, closed, epsilon, mesh) {
|
|
60941
61020
|
const pos = flatten(verts, closed, epsilon, mesh);
|
|
60942
61021
|
if (pos.length < 6)
|
|
60943
|
-
return; // Line2
|
|
60944
|
-
//
|
|
60945
|
-
//
|
|
61022
|
+
return; // Line2 needs >=2 points
|
|
61023
|
+
// Important: calling setPositions on the existing geometry to grow it only renders the first
|
|
61024
|
+
// segment (a known pitfall). Instead, rebuild and swap in a new LineGeometry each time so the
|
|
61025
|
+
// instance count is correct and every segment renders.
|
|
60946
61026
|
const geo = new LineGeometry();
|
|
60947
61027
|
geo.setPositions(pos);
|
|
60948
61028
|
const old = line.geometry;
|
|
@@ -60950,20 +61030,22 @@ function updateContourLine(line, verts, closed, epsilon, mesh) {
|
|
|
60950
61030
|
old.dispose();
|
|
60951
61031
|
line.computeLineDistances();
|
|
60952
61032
|
}
|
|
60953
|
-
/**
|
|
61033
|
+
/** Change the color of an existing contour line. */
|
|
60954
61034
|
function setContourColor(line, color) {
|
|
60955
61035
|
line.material.color.set(color);
|
|
60956
61036
|
}
|
|
60957
61037
|
|
|
60958
61038
|
/**
|
|
60959
|
-
*
|
|
60960
|
-
*
|
|
61039
|
+
* Build a vertex adjacency graph from a BufferGeometry's index, used for the shortest path in
|
|
61040
|
+
* mode B (geodesic). Vertex coordinates are stored in local space; query points must be converted
|
|
61041
|
+
* to local first (mesh.worldToLocal).
|
|
60961
61042
|
*
|
|
60962
|
-
*
|
|
60963
|
-
*
|
|
61043
|
+
* Note: the geometry must be indexed. modelLoader welds duplicate vertices with mergeVertices so
|
|
61044
|
+
* that vertices at the same surface position are shared, otherwise the adjacency graph won't be connected.
|
|
60964
61045
|
*
|
|
60965
|
-
*
|
|
60966
|
-
*
|
|
61046
|
+
* Performance: O(V²) Dijkstra + O(V) nearest-vertex lookup. Acceptable for a single click on tens
|
|
61047
|
+
* of thousands of vertices; if too large it can later be sped up with a binary heap + spatial grid
|
|
61048
|
+
* (left as a future upgrade).
|
|
60967
61049
|
*/
|
|
60968
61050
|
class MeshGraph {
|
|
60969
61051
|
constructor(geometry) {
|
|
@@ -60991,7 +61073,7 @@ class MeshGraph {
|
|
|
60991
61073
|
}
|
|
60992
61074
|
}
|
|
60993
61075
|
else {
|
|
60994
|
-
//
|
|
61076
|
+
// Non-indexed fallback (poor connectivity, only to avoid crashing).
|
|
60995
61077
|
for (let i = 0; i + 2 < this.vertexCount; i += 3) {
|
|
60996
61078
|
addEdge(i, i + 1);
|
|
60997
61079
|
addEdge(i + 1, i + 2);
|
|
@@ -61008,7 +61090,7 @@ class MeshGraph {
|
|
|
61008
61090
|
pz(i) {
|
|
61009
61091
|
return this.positions[i * 3 + 2];
|
|
61010
61092
|
}
|
|
61011
|
-
/**
|
|
61093
|
+
/** Nearest vertex (pass in local coordinates). */
|
|
61012
61094
|
nearestVertex(localPoint) {
|
|
61013
61095
|
let best = -1;
|
|
61014
61096
|
let bestD = Infinity;
|
|
@@ -61025,8 +61107,8 @@ class MeshGraph {
|
|
|
61025
61107
|
return best;
|
|
61026
61108
|
}
|
|
61027
61109
|
/**
|
|
61028
|
-
*
|
|
61029
|
-
*
|
|
61110
|
+
* Shortest path between two vertices (sequence of vertex indices including endpoints).
|
|
61111
|
+
* Binary-heap Dijkstra, O(E log V). Falls back to [start,end] when disconnected.
|
|
61030
61112
|
*/
|
|
61031
61113
|
shortestPath(startV, endV) {
|
|
61032
61114
|
if (startV === endV)
|
|
@@ -61083,7 +61165,7 @@ class MeshGraph {
|
|
|
61083
61165
|
const nm = new Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
61084
61166
|
return n.applyMatrix3(nm).normalize();
|
|
61085
61167
|
}
|
|
61086
|
-
/**
|
|
61168
|
+
/** Vertex local coordinates + local normal (the graph geometry is already local space). */
|
|
61087
61169
|
vertexLocal(i) {
|
|
61088
61170
|
return {
|
|
61089
61171
|
x: this.px(i),
|
|
@@ -61096,7 +61178,7 @@ class MeshGraph {
|
|
|
61096
61178
|
};
|
|
61097
61179
|
}
|
|
61098
61180
|
}
|
|
61099
|
-
/**
|
|
61181
|
+
/** Minimal binary min-heap (ordered by priority, stores vertex indices). Used for Dijkstra. */
|
|
61100
61182
|
class MinHeap {
|
|
61101
61183
|
constructor() {
|
|
61102
61184
|
this.ids = [];
|
|
@@ -61153,22 +61235,23 @@ class MinHeap {
|
|
|
61153
61235
|
}
|
|
61154
61236
|
|
|
61155
61237
|
/**
|
|
61156
|
-
*
|
|
61157
|
-
*
|
|
61238
|
+
* State machine for mode B (geodesic): click anchors one by one, find the shortest path between
|
|
61239
|
+
* adjacent anchors with MeshGraph, and stitch them into a polyline that hugs the surface.
|
|
61240
|
+
* Enter finishes and can close the loop (computing one more segment between first and last).
|
|
61158
61241
|
*/
|
|
61159
61242
|
class GeodesicContour {
|
|
61160
61243
|
constructor(graph, mesh) {
|
|
61161
61244
|
this.graph = graph;
|
|
61162
61245
|
this.mesh = mesh;
|
|
61163
|
-
this.anchors = []; //
|
|
61164
|
-
this.segments = []; //
|
|
61165
|
-
this.history = []; //
|
|
61246
|
+
this.anchors = []; // vertex indices
|
|
61247
|
+
this.segments = []; // path between adjacent anchors (endpoints included)
|
|
61248
|
+
this.history = []; // anchor snapshot before each add/remove, for undo
|
|
61166
61249
|
}
|
|
61167
|
-
/**
|
|
61250
|
+
/** Save a snapshot before any anchor change (the vertex-index array is tiny, so snapshot cost is negligible). */
|
|
61168
61251
|
snapshot() {
|
|
61169
61252
|
this.history.push(this.anchors.slice());
|
|
61170
61253
|
}
|
|
61171
|
-
/**
|
|
61254
|
+
/** Given a local hit point, snap to the nearest vertex and compute the path to the previous anchor. */
|
|
61172
61255
|
addAnchor(localHitPoint) {
|
|
61173
61256
|
const v = this.graph.nearestVertex(localHitPoint);
|
|
61174
61257
|
this.snapshot();
|
|
@@ -61178,7 +61261,7 @@ class GeodesicContour {
|
|
|
61178
61261
|
}
|
|
61179
61262
|
this.anchors.push(v);
|
|
61180
61263
|
}
|
|
61181
|
-
/**
|
|
61264
|
+
/** Remove the anchor at the given index and recompute adjacent segments (allows canceling any middle point). */
|
|
61182
61265
|
removeAnchorAt(index) {
|
|
61183
61266
|
if (index < 0 || index >= this.anchors.length)
|
|
61184
61267
|
return;
|
|
@@ -61186,11 +61269,11 @@ class GeodesicContour {
|
|
|
61186
61269
|
this.anchors.splice(index, 1);
|
|
61187
61270
|
this.rebuildSegments();
|
|
61188
61271
|
}
|
|
61189
|
-
/**
|
|
61272
|
+
/** Whether there is still an editable change to undo (add/remove point). */
|
|
61190
61273
|
canUndo() {
|
|
61191
61274
|
return this.history.length > 0;
|
|
61192
61275
|
}
|
|
61193
|
-
/**
|
|
61276
|
+
/** Undo the last anchor edit (add → remove it; remove → restore it). Returns false when no history. */
|
|
61194
61277
|
undoEdit() {
|
|
61195
61278
|
const prev = this.history.pop();
|
|
61196
61279
|
if (!prev)
|
|
@@ -61199,21 +61282,21 @@ class GeodesicContour {
|
|
|
61199
61282
|
this.rebuildSegments();
|
|
61200
61283
|
return true;
|
|
61201
61284
|
}
|
|
61202
|
-
/**
|
|
61285
|
+
/** Recompute all adjacent-segment paths from the current anchor sequence. */
|
|
61203
61286
|
rebuildSegments() {
|
|
61204
61287
|
this.segments = [];
|
|
61205
61288
|
for (let i = 1; i < this.anchors.length; i++) {
|
|
61206
61289
|
this.segments.push(this.graph.shortestPath(this.anchors[i - 1], this.anchors[i]));
|
|
61207
61290
|
}
|
|
61208
61291
|
}
|
|
61209
|
-
/**
|
|
61292
|
+
/** Local vertex of each anchor (used to draw visible anchor markers). */
|
|
61210
61293
|
getAnchorLocals() {
|
|
61211
61294
|
return this.anchors.map((i) => this.graph.vertexLocal(i));
|
|
61212
61295
|
}
|
|
61213
61296
|
get anchorCount() {
|
|
61214
61297
|
return this.anchors.length;
|
|
61215
61298
|
}
|
|
61216
|
-
/**
|
|
61299
|
+
/** Stitch all path vertices (world) + normals into a polyline; when closed, add one first-to-last segment. */
|
|
61217
61300
|
buildVertices(closed) {
|
|
61218
61301
|
const segs = this.segments.slice();
|
|
61219
61302
|
if (closed && this.anchors.length > 2) {
|
|
@@ -61221,22 +61304,22 @@ class GeodesicContour {
|
|
|
61221
61304
|
}
|
|
61222
61305
|
const idxPath = [];
|
|
61223
61306
|
segs.forEach((s, si) => {
|
|
61224
|
-
const start = si === 0 ? 0 : 1; //
|
|
61307
|
+
const start = si === 0 ? 0 : 1; // dedupe the endpoint shared between adjacent segments
|
|
61225
61308
|
for (let k = start; k < s.length; k++)
|
|
61226
61309
|
idxPath.push(s[k]);
|
|
61227
61310
|
});
|
|
61228
61311
|
if (idxPath.length === 0 && this.anchors.length === 1) {
|
|
61229
61312
|
idxPath.push(this.anchors[0]);
|
|
61230
61313
|
}
|
|
61231
|
-
//
|
|
61314
|
+
// The graph geometry is already local space, so emit local vertices directly (world is derived at render time).
|
|
61232
61315
|
return idxPath.map((i) => this.graph.vertexLocal(i));
|
|
61233
61316
|
}
|
|
61234
61317
|
}
|
|
61235
61318
|
|
|
61236
61319
|
/**
|
|
61237
|
-
*
|
|
61238
|
-
*
|
|
61239
|
-
*
|
|
61320
|
+
* Annotation data model + undo stack + export. Pure data, no three / DOM dependencies.
|
|
61321
|
+
* Undo only records two atomic operations, add / remove (enough for "undo last add / restore delete").
|
|
61322
|
+
* Notifies the UI and render layer via subscribe (the latter reconciles the scene against list()).
|
|
61240
61323
|
*/
|
|
61241
61324
|
class AnnotationStore {
|
|
61242
61325
|
constructor() {
|
|
@@ -61279,7 +61362,7 @@ class AnnotationStore {
|
|
|
61279
61362
|
}
|
|
61280
61363
|
this.notify();
|
|
61281
61364
|
}
|
|
61282
|
-
/**
|
|
61365
|
+
/** Clear all, returning the removed items (so the render layer can dispose three objects). */
|
|
61283
61366
|
clear() {
|
|
61284
61367
|
const old = this.items;
|
|
61285
61368
|
this.items = [];
|
|
@@ -61340,13 +61423,14 @@ class AnnotationStore {
|
|
|
61340
61423
|
const LINE_W = 3;
|
|
61341
61424
|
const LINE_W_SEL = 6;
|
|
61342
61425
|
/**
|
|
61343
|
-
*
|
|
61344
|
-
* Enter
|
|
61345
|
-
*
|
|
61426
|
+
* Main surface-annotation controller (Phase 4): four modes navigate / freehand / geodesic / point,
|
|
61427
|
+
* Enter closes the loop, and data is managed by AnnotationStore (multiple color-labeled strips,
|
|
61428
|
+
* undo/delete/clear, export).
|
|
61429
|
+
* The render layer reconciles the scene against store.list(); undo/delete automatically add/remove three objects.
|
|
61346
61430
|
*/
|
|
61347
61431
|
class SurfaceAnnotator {
|
|
61348
61432
|
constructor(opts) {
|
|
61349
|
-
var _a, _b;
|
|
61433
|
+
var _a, _b, _c;
|
|
61350
61434
|
this.mode = "navigate";
|
|
61351
61435
|
this.spaceHeld = false;
|
|
61352
61436
|
this.pointerDown = false;
|
|
@@ -61354,12 +61438,12 @@ class SurfaceAnnotator {
|
|
|
61354
61438
|
this.managed = new Set();
|
|
61355
61439
|
this.seq = 0;
|
|
61356
61440
|
this.selectedId = null;
|
|
61357
|
-
this.activeGeoMarkers = []; //
|
|
61441
|
+
this.activeGeoMarkers = []; // visible anchors of the in-progress geodesic
|
|
61358
61442
|
this.geoRay = new Raycaster();
|
|
61359
61443
|
this.geoNdc = new Vector2();
|
|
61360
61444
|
this.hoveredGeoMarker = -1;
|
|
61361
61445
|
this._projV = new Vector3();
|
|
61362
|
-
/**
|
|
61446
|
+
/** Update the pixel resolution of all fat lines on window resize, otherwise line width distorts. */
|
|
61363
61447
|
this.onResize = () => {
|
|
61364
61448
|
const w = this.o.container.clientWidth;
|
|
61365
61449
|
const h = this.o.container.clientHeight;
|
|
@@ -61378,7 +61462,7 @@ class SurfaceAnnotator {
|
|
|
61378
61462
|
if (this.spaceHeld)
|
|
61379
61463
|
return;
|
|
61380
61464
|
if (e.button !== 0)
|
|
61381
|
-
return; //
|
|
61465
|
+
return; // left button only
|
|
61382
61466
|
if (!this.insideContainer(e))
|
|
61383
61467
|
return;
|
|
61384
61468
|
this.pointerDown = true;
|
|
@@ -61413,7 +61497,7 @@ class SurfaceAnnotator {
|
|
|
61413
61497
|
return;
|
|
61414
61498
|
}
|
|
61415
61499
|
if (this.mode === "geodesic") {
|
|
61416
|
-
//
|
|
61500
|
+
// First check whether an existing anchor was clicked → cancel that point (any point can be canceled).
|
|
61417
61501
|
const pick = this.pickGeoMarker(e);
|
|
61418
61502
|
if (pick >= 0 && this.activeGeo) {
|
|
61419
61503
|
this.activeGeo.removeAnchorAt(pick);
|
|
@@ -61425,7 +61509,7 @@ class SurfaceAnnotator {
|
|
|
61425
61509
|
}
|
|
61426
61510
|
return;
|
|
61427
61511
|
}
|
|
61428
|
-
//
|
|
61512
|
+
// Otherwise drop a new anchor on the surface.
|
|
61429
61513
|
const h = this.hit(e);
|
|
61430
61514
|
if (!h)
|
|
61431
61515
|
return;
|
|
@@ -61443,7 +61527,7 @@ class SurfaceAnnotator {
|
|
|
61443
61527
|
this.setGeoHover(-1);
|
|
61444
61528
|
return;
|
|
61445
61529
|
}
|
|
61446
|
-
//
|
|
61530
|
+
// In geodesic mode, show the "✕" hint when hovering an anchor sphere (that point can be clicked to cancel).
|
|
61447
61531
|
if (this.mode === "geodesic" && !this.pointerDown) {
|
|
61448
61532
|
this.setGeoHover(this.insideContainer(e) ? this.pickGeoMarker(e) : -1);
|
|
61449
61533
|
}
|
|
@@ -61508,7 +61592,7 @@ class SurfaceAnnotator {
|
|
|
61508
61592
|
return;
|
|
61509
61593
|
}
|
|
61510
61594
|
if (e.key === "Enter") {
|
|
61511
|
-
//
|
|
61595
|
+
// Geodesic in progress → finish and close it; otherwise close the most recent freehand line.
|
|
61512
61596
|
if (this.activeGeo)
|
|
61513
61597
|
this.finishGeodesic();
|
|
61514
61598
|
else
|
|
@@ -61533,24 +61617,25 @@ class SurfaceAnnotator {
|
|
|
61533
61617
|
this.o = opts;
|
|
61534
61618
|
const meshGeo = opts.mesh.geometry;
|
|
61535
61619
|
if (!meshGeo.getAttribute("normal"))
|
|
61536
|
-
meshGeo.computeVertexNormals(); //
|
|
61620
|
+
meshGeo.computeVertexNormals(); // rendering needs normals
|
|
61537
61621
|
meshGeo.computeBoundingBox();
|
|
61538
61622
|
const diag = (_a = opts.bboxDiagonal) !== null && _a !== void 0 ? _a : meshGeo.boundingBox.getSize(new Vector3()).length();
|
|
61539
61623
|
this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.0032;
|
|
61540
|
-
this.epsilon = diag * 0.002;
|
|
61624
|
+
this.epsilon = (_c = opts.epsilon) !== null && _c !== void 0 ? _c : diag * 0.002;
|
|
61541
61625
|
this.minGap = diag * 0.004;
|
|
61542
61626
|
this.maxJump = diag * 0.05;
|
|
61543
|
-
//
|
|
61544
|
-
//
|
|
61545
|
-
//
|
|
61627
|
+
// Geodesic connectivity: weld a separate "position-only" geometry for MeshGraph to use —
|
|
61628
|
+
// leaving the rendered mesh untouched (keeping its normals/UV/texture). Position-only ensures
|
|
61629
|
+
// vertices at the same surface position get merged by mergeVertices (otherwise per-face
|
|
61630
|
+
// normals/UV would block the merge, breaking the graph → the closed loop cuts through the model).
|
|
61546
61631
|
const posOnly = new BufferGeometry();
|
|
61547
61632
|
posOnly.setAttribute("position", meshGeo.getAttribute("position").clone());
|
|
61548
61633
|
const graphGeo = mergeVertices(posOnly);
|
|
61549
61634
|
graphGeo.computeVertexNormals();
|
|
61550
61635
|
this.graph = new MeshGraph(graphGeo);
|
|
61551
61636
|
this.store.subscribe(() => this.reconcile());
|
|
61552
|
-
//
|
|
61553
|
-
//
|
|
61637
|
+
// Listen on window in the capture phase (capture=true): receive every pointer event before
|
|
61638
|
+
// copper's TrackballControls, so its setPointerCapture / event routing doesn't drop moves mid-drag.
|
|
61554
61639
|
window.addEventListener("pointerdown", this.onPointerDown, true);
|
|
61555
61640
|
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
61556
61641
|
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
@@ -61571,13 +61656,13 @@ class SurfaceAnnotator {
|
|
|
61571
61656
|
var _a;
|
|
61572
61657
|
return (_a = this.o.pointColor) !== null && _a !== void 0 ? _a : "#ffd166";
|
|
61573
61658
|
}
|
|
61574
|
-
// ----
|
|
61659
|
+
// ---- Public API (called from Vue) ----
|
|
61575
61660
|
getMode() {
|
|
61576
61661
|
return this.mode;
|
|
61577
61662
|
}
|
|
61578
61663
|
setMode(m) {
|
|
61579
61664
|
var _a, _b;
|
|
61580
|
-
//
|
|
61665
|
+
// When leaving geodesic mode, discard the in-progress geodesic not yet committed with Enter (clear leftover anchors and line).
|
|
61581
61666
|
if (m !== "geodesic" && this.activeGeo)
|
|
61582
61667
|
this.clearActiveGeo();
|
|
61583
61668
|
this.mode = m;
|
|
@@ -61587,13 +61672,13 @@ class SurfaceAnnotator {
|
|
|
61587
61672
|
getStore() {
|
|
61588
61673
|
return this.store;
|
|
61589
61674
|
}
|
|
61590
|
-
/**
|
|
61675
|
+
/** Snapshot of the current annotation list. */
|
|
61591
61676
|
getAnnotations() {
|
|
61592
61677
|
return this.store.list();
|
|
61593
61678
|
}
|
|
61594
61679
|
undo() {
|
|
61595
|
-
//
|
|
61596
|
-
//
|
|
61680
|
+
// Geodesic in progress → undo the most recent anchor edit (both adding and canceling a point can be rolled back);
|
|
61681
|
+
// otherwise undo the most recently committed annotation.
|
|
61597
61682
|
if (this.activeGeo && this.activeGeo.canUndo()) {
|
|
61598
61683
|
this.activeGeo.undoEdit();
|
|
61599
61684
|
if (this.activeGeo.anchorCount === 0)
|
|
@@ -61627,7 +61712,7 @@ class SurfaceAnnotator {
|
|
|
61627
61712
|
this.selectedId = id;
|
|
61628
61713
|
this.applySelection();
|
|
61629
61714
|
}
|
|
61630
|
-
/**
|
|
61715
|
+
/** Redraw the corresponding three object after a color change. */
|
|
61631
61716
|
refreshAnnotation(id) {
|
|
61632
61717
|
const a = this.store.get(id);
|
|
61633
61718
|
if (!a || !a.object3D)
|
|
@@ -61643,12 +61728,12 @@ class SurfaceAnnotator {
|
|
|
61643
61728
|
exportJSON(modelName, opts) {
|
|
61644
61729
|
return this.store.toJSON(modelName, this.o.mesh, opts);
|
|
61645
61730
|
}
|
|
61646
|
-
// ----
|
|
61731
|
+
// ---- Internal ----
|
|
61647
61732
|
applyCameraGating() {
|
|
61648
|
-
//
|
|
61733
|
+
// Holding Space for temporary rotation takes priority; otherwise enable the camera only in navigate mode.
|
|
61649
61734
|
this.o.controls.enabled = this.spaceHeld || this.mode === "navigate";
|
|
61650
61735
|
}
|
|
61651
|
-
/**
|
|
61736
|
+
/** Reconcile the scene against store.list(): add missing objects, remove deleted ones (no dispose, kept for undo restore). */
|
|
61652
61737
|
reconcile() {
|
|
61653
61738
|
var _a, _b;
|
|
61654
61739
|
const present = new Set();
|
|
@@ -61698,29 +61783,53 @@ class SurfaceAnnotator {
|
|
|
61698
61783
|
return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
|
|
61699
61784
|
}
|
|
61700
61785
|
/**
|
|
61701
|
-
*
|
|
61702
|
-
*
|
|
61703
|
-
*
|
|
61704
|
-
*
|
|
61786
|
+
* Whether the event target is the WebGL canvas inside the container.
|
|
61787
|
+
* Only accept the canvas: panels (GUIDE / control panel / the ✕ buttons in the annotation list)
|
|
61788
|
+
* are all children of the container, so checking contains alone would treat clicks on the panels
|
|
61789
|
+
* as drawing on the model — which "leaks through" the delete buttons and makes ✕ hard to click.
|
|
61790
|
+
* Responding only to pointer events on the canvas fully isolates the UI from the drawing surface.
|
|
61705
61791
|
*/
|
|
61706
61792
|
insideContainer(e) {
|
|
61707
61793
|
const t = e.target;
|
|
61708
61794
|
return !!t && t.tagName === "CANVAS" && this.o.container.contains(t);
|
|
61709
61795
|
}
|
|
61710
|
-
/**
|
|
61796
|
+
/**
|
|
61797
|
+
* Pick an in-progress anchor: return the index of the nearest anchor (screen pixel distance <
|
|
61798
|
+
* tolerance), or -1 on a miss.
|
|
61799
|
+
*
|
|
61800
|
+
* Uses "screen-space pixel distance" instead of ray/sphere intersection: the anchor spheres are
|
|
61801
|
+
* tiny, so strict ray/sphere hits require pixel-perfect aiming and most clicks miss — and a miss
|
|
61802
|
+
* falls through to the "add anchor" branch, so trying to delete a point ends up adding one. That
|
|
61803
|
+
* was the root cause of the old "deleting a point takes several clicks, must hit the sphere dead
|
|
61804
|
+
* center" problem. Projecting each anchor to the screen and taking the nearest within tolerance
|
|
61805
|
+
* makes deletion stable and reliable. The tolerance is enlarged relative to the sphere's screen
|
|
61806
|
+
* radius to make it easy to hit.
|
|
61807
|
+
*/
|
|
61711
61808
|
pickGeoMarker(e) {
|
|
61712
61809
|
if (!this.activeGeoMarkers.length)
|
|
61713
61810
|
return -1;
|
|
61714
61811
|
const rect = this.o.container.getBoundingClientRect();
|
|
61715
|
-
|
|
61716
|
-
|
|
61717
|
-
|
|
61718
|
-
|
|
61719
|
-
|
|
61720
|
-
|
|
61721
|
-
|
|
61812
|
+
const px = e.clientX - rect.left;
|
|
61813
|
+
const py = e.clientY - rect.top;
|
|
61814
|
+
const TOL = 16; // hit tolerance (pixels), larger than the sphere itself for forgiving clicks
|
|
61815
|
+
let best = -1;
|
|
61816
|
+
let bestDist = TOL;
|
|
61817
|
+
const v = this._projV;
|
|
61818
|
+
for (let i = 0; i < this.activeGeoMarkers.length; i++) {
|
|
61819
|
+
v.copy(this.activeGeoMarkers[i].position).project(this.o.camera);
|
|
61820
|
+
if (v.z > 1)
|
|
61821
|
+
continue; // projects behind the camera, skip
|
|
61822
|
+
const sx = (v.x * 0.5 + 0.5) * rect.width;
|
|
61823
|
+
const sy = (-v.y * 0.5 + 0.5) * rect.height;
|
|
61824
|
+
const d = Math.hypot(sx - px, sy - py);
|
|
61825
|
+
if (d < bestDist) {
|
|
61826
|
+
bestDist = d;
|
|
61827
|
+
best = i;
|
|
61828
|
+
}
|
|
61829
|
+
}
|
|
61830
|
+
return best;
|
|
61722
61831
|
}
|
|
61723
|
-
/**
|
|
61832
|
+
/** Set the currently hovered anchor (-1 = none): update highlight scale, cursor, and the "✕ (cancel)" floating badge. */
|
|
61724
61833
|
setGeoHover(idx) {
|
|
61725
61834
|
if (idx === this.hoveredGeoMarker) {
|
|
61726
61835
|
if (idx >= 0)
|
|
@@ -61744,7 +61853,7 @@ class SurfaceAnnotator {
|
|
|
61744
61853
|
this.o.container.style.cursor = "";
|
|
61745
61854
|
}
|
|
61746
61855
|
}
|
|
61747
|
-
/**
|
|
61856
|
+
/** Lazily create the floating ✕ badge (appended inside the container, pointer-events:none so it doesn't block clicks). */
|
|
61748
61857
|
ensureGeoBadge() {
|
|
61749
61858
|
if (this.geoHoverBadge)
|
|
61750
61859
|
return this.geoHoverBadge;
|
|
@@ -61772,7 +61881,7 @@ class SurfaceAnnotator {
|
|
|
61772
61881
|
this.geoHoverBadge = b;
|
|
61773
61882
|
return b;
|
|
61774
61883
|
}
|
|
61775
|
-
/**
|
|
61884
|
+
/** Position the ✕ badge at the anchor's screen projection (dead center, i.e. where the cursor hovers, to avoid ambiguity). */
|
|
61776
61885
|
positionGeoBadge(marker) {
|
|
61777
61886
|
if (!marker)
|
|
61778
61887
|
return;
|
|
@@ -61784,7 +61893,7 @@ class SurfaceAnnotator {
|
|
|
61784
61893
|
b.style.left = `${x}px`;
|
|
61785
61894
|
b.style.top = `${y}px`;
|
|
61786
61895
|
}
|
|
61787
|
-
/**
|
|
61896
|
+
/** Rebuild the visible anchor spheres from the current anchors (slightly larger than placed points, so they're easy to see and click to cancel). */
|
|
61788
61897
|
rebuildGeoMarkers() {
|
|
61789
61898
|
this.setGeoHover(-1);
|
|
61790
61899
|
for (const m of this.activeGeoMarkers) {
|
|
@@ -61800,7 +61909,7 @@ class SurfaceAnnotator {
|
|
|
61800
61909
|
this.activeGeoMarkers.push(marker);
|
|
61801
61910
|
}
|
|
61802
61911
|
}
|
|
61803
|
-
/**
|
|
61912
|
+
/** Redraw the in-progress geodesic from the current anchors (not closed); remove the line when fewer than two points. */
|
|
61804
61913
|
redrawGeoLine() {
|
|
61805
61914
|
if (!this.activeGeo)
|
|
61806
61915
|
return;
|
|
@@ -61821,7 +61930,7 @@ class SurfaceAnnotator {
|
|
|
61821
61930
|
updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
|
|
61822
61931
|
}
|
|
61823
61932
|
}
|
|
61824
|
-
/**
|
|
61933
|
+
/** Discard the in-progress geodesic: remove and dispose the line and all anchors. */
|
|
61825
61934
|
clearActiveGeo() {
|
|
61826
61935
|
this.setGeoHover(-1);
|
|
61827
61936
|
if (this.activeGeoLine) {
|
|
@@ -61836,7 +61945,7 @@ class SurfaceAnnotator {
|
|
|
61836
61945
|
this.activeGeoMarkers = [];
|
|
61837
61946
|
this.activeGeo = undefined;
|
|
61838
61947
|
}
|
|
61839
|
-
/**
|
|
61948
|
+
/** Remove all anchor spheres of the in-progress geodesic (called after committing: only the line remains). */
|
|
61840
61949
|
removeGeoMarkers() {
|
|
61841
61950
|
this.setGeoHover(-1);
|
|
61842
61951
|
for (const m of this.activeGeoMarkers) {
|
|
@@ -61849,7 +61958,7 @@ class SurfaceAnnotator {
|
|
|
61849
61958
|
const last = this.lastFreehand;
|
|
61850
61959
|
if (!last || last.closed || last.vertices.length < 3 || !last.object3D)
|
|
61851
61960
|
return;
|
|
61852
|
-
//
|
|
61961
|
+
// Use a geodesic along the surface to connect the last point back to the first, so a straight chord doesn't "shortcut" through the model interior and get occluded.
|
|
61853
61962
|
const tail = last.vertices[last.vertices.length - 1];
|
|
61854
61963
|
const head = last.vertices[0];
|
|
61855
61964
|
const closing = this.surfacePathBetween(tail, head);
|
|
@@ -61857,14 +61966,14 @@ class SurfaceAnnotator {
|
|
|
61857
61966
|
last.closed = true;
|
|
61858
61967
|
updateContourLine(last.object3D, last.vertices, true, this.epsilon, this.o.mesh);
|
|
61859
61968
|
}
|
|
61860
|
-
/**
|
|
61969
|
+
/** Compute the geodesic path a→b along the mesh surface (local vertices), dropping the first point that coincides with a. a/b are already local. */
|
|
61861
61970
|
surfacePathBetween(a, b) {
|
|
61862
61971
|
const va = this.graph.nearestVertex(new Vector3(a.x, a.y, a.z));
|
|
61863
61972
|
const vb = this.graph.nearestVertex(new Vector3(b.x, b.y, b.z));
|
|
61864
61973
|
const path = this.graph.shortestPath(va, vb);
|
|
61865
61974
|
return path.slice(1).map((i) => this.graph.vertexLocal(i));
|
|
61866
61975
|
}
|
|
61867
|
-
/**
|
|
61976
|
+
/** Finish the current geodesic: close it into a ring and commit it as one contour. */
|
|
61868
61977
|
finishGeodesic() {
|
|
61869
61978
|
if (!this.activeGeo || !this.activeGeoLine)
|
|
61870
61979
|
return;
|
|
@@ -61888,7 +61997,7 @@ class SurfaceAnnotator {
|
|
|
61888
61997
|
this.o.scene.remove(this.activeGeoLine);
|
|
61889
61998
|
this.disposeObject(this.activeGeoLine);
|
|
61890
61999
|
}
|
|
61891
|
-
//
|
|
62000
|
+
// After committing, clear the visible anchors and leave only the surface-hugging line.
|
|
61892
62001
|
this.removeGeoMarkers();
|
|
61893
62002
|
this.activeGeo = undefined;
|
|
61894
62003
|
this.activeGeoLine = undefined;
|
|
@@ -62003,9 +62112,9 @@ class copperScene extends baseScene {
|
|
|
62003
62112
|
copperVtkLoader(url, this.scene, this.content);
|
|
62004
62113
|
}
|
|
62005
62114
|
/**
|
|
62006
|
-
*
|
|
62007
|
-
* target
|
|
62008
|
-
*
|
|
62115
|
+
* Create an annotator on the surface of a given model (draw contours / place points / export coordinates).
|
|
62116
|
+
* target can be a single Mesh, or a Group/Object3D (the mesh with the most vertices is chosen automatically).
|
|
62117
|
+
* Reuses this scene's camera / container / controls; non-indexed geometry is welded and indexed automatically.
|
|
62009
62118
|
*/
|
|
62010
62119
|
createSurfaceAnnotator(target, opts) {
|
|
62011
62120
|
const mesh = this.pickAnnotatableMesh(target);
|
|
@@ -62033,7 +62142,7 @@ class copperScene extends baseScene {
|
|
|
62033
62142
|
});
|
|
62034
62143
|
return best;
|
|
62035
62144
|
}
|
|
62036
|
-
/**
|
|
62145
|
+
/** Dispose all annotators created on this scene (removes event listeners and annotation objects). */
|
|
62037
62146
|
disposeSurfaceAnnotators() {
|
|
62038
62147
|
this.surfaceAnnotators.forEach((a) => a.dispose());
|
|
62039
62148
|
this.surfaceAnnotators = [];
|
|
@@ -79007,14 +79116,29 @@ class AiAssistTool extends BaseTool {
|
|
|
79007
79116
|
this.committedVol = null;
|
|
79008
79117
|
this.promptTool = "point";
|
|
79009
79118
|
this.polarity = 1; // 1 = foreground (positive), 0 = background
|
|
79010
|
-
/**
|
|
79011
|
-
|
|
79119
|
+
/** The label value (1-255) the AI currently paints into = the active Segmentation.
|
|
79120
|
+
* Each Segmentation is a label value in the single-channel scratch volume; its
|
|
79121
|
+
* colour lives in the volume's colorMap (set via setSegmentColor). */
|
|
79122
|
+
this.activeLabel = 1;
|
|
79012
79123
|
/** Scribble brush radius (px), user-adjustable via slider. */
|
|
79013
79124
|
this.scribbleSize = 5;
|
|
79014
79125
|
/** Accumulated prompts for the CURRENT slice; reset on slice/axis change. */
|
|
79015
79126
|
this.points = [];
|
|
79016
79127
|
this.scribble = [];
|
|
79128
|
+
/** Lasso contour sent to the backend (densified along the curve at finish). */
|
|
79129
|
+
this.lasso = [];
|
|
79017
79130
|
this.activeSlice = -1;
|
|
79131
|
+
// ── Lasso v2 editing state (discrete clicked vertices → smooth closed curve) ──
|
|
79132
|
+
/** Placed lasso vertices (voxel + screen + polarity). Empty = not editing. */
|
|
79133
|
+
this.lassoVerts = [];
|
|
79134
|
+
/** Undo / redo snapshot stacks of the vertex list (for Ctrl+Z / Ctrl+Y). */
|
|
79135
|
+
this.lassoUndo = [];
|
|
79136
|
+
this.lassoRedo = [];
|
|
79137
|
+
/** Index of the vertex currently hovered (shows a red ✕; click removes it). −1 = none. */
|
|
79138
|
+
this.lassoHoverIdx = -1;
|
|
79139
|
+
/** Double-click detection (a dbl-click finishes the lasso). */
|
|
79140
|
+
this.lastLassoDownTime = 0;
|
|
79141
|
+
this.lastLassoDownPos = { x: 0, y: 0 };
|
|
79018
79142
|
// Drag state (box / scribble) — voxel coords for the prompt
|
|
79019
79143
|
this.dragging = false;
|
|
79020
79144
|
this.dragStart = null;
|
|
@@ -79025,8 +79149,14 @@ class AiAssistTool extends BaseTool {
|
|
|
79025
79149
|
// Live cursor position (screen px) for the scribble brush-size preview ring —
|
|
79026
79150
|
// updated on every hover move so the ring follows the mouse like the paint brush.
|
|
79027
79151
|
this.hoverScreen = null;
|
|
79152
|
+
/** Hide the point seed markers once a prediction has painted (only the mask
|
|
79153
|
+
* should remain). A new point click re-shows them until its mask returns. */
|
|
79154
|
+
this.hidePointMarkers = false;
|
|
79028
79155
|
/** Fired when a prompt gesture completes — app calls backend, then applyMask(). */
|
|
79029
79156
|
this.onPrompt = null;
|
|
79157
|
+
/** Fired whenever the lasso vertex set changes (add/delete/undo/redo/finish/cancel),
|
|
79158
|
+
* so the panel can reactively show the Finish button + vertex count. */
|
|
79159
|
+
this.onLassoChange = null;
|
|
79030
79160
|
this.host = host;
|
|
79031
79161
|
}
|
|
79032
79162
|
// ── Configuration (driven from the panel via NrrdTools) ────────────────────
|
|
@@ -79044,16 +79174,51 @@ class AiAssistTool extends BaseTool {
|
|
|
79044
79174
|
setScribbleSize(size) {
|
|
79045
79175
|
this.scribbleSize = Math.max(1, Math.min(40, Math.round(size)));
|
|
79046
79176
|
}
|
|
79047
|
-
|
|
79048
|
-
|
|
79049
|
-
|
|
79050
|
-
|
|
79051
|
-
|
|
79052
|
-
if (c !== this.
|
|
79177
|
+
/** Select the active Segmentation by its label value (1-255). Switching starts a
|
|
79178
|
+
* fresh prompt set — otherwise accumulated prompts would be re-predicted and
|
|
79179
|
+
* repainted into the new label, recolouring the previous segmentation's region. */
|
|
79180
|
+
setActiveSegment(label) {
|
|
79181
|
+
const c = Math.max(1, Math.min(255, Math.round(label)));
|
|
79182
|
+
if (c !== this.activeLabel)
|
|
79053
79183
|
this.resetPrompts();
|
|
79054
|
-
this.
|
|
79184
|
+
this.activeLabel = c;
|
|
79185
|
+
}
|
|
79186
|
+
getActiveLabel() { return this.activeLabel; }
|
|
79187
|
+
/** Set a Segmentation's colour (its label's entry in the scratch volume colorMap).
|
|
79188
|
+
* The 2D overlay repaints next frame (copper3d's render loop re-reads the map). */
|
|
79189
|
+
setSegmentColor(label, color) {
|
|
79190
|
+
var _a;
|
|
79191
|
+
(_a = this.scratchVol) === null || _a === void 0 ? void 0 : _a.setChannelColor(Math.round(label), color);
|
|
79192
|
+
}
|
|
79193
|
+
/** "New segmentation": freeze the current regions (so they persist) and switch to
|
|
79194
|
+
* a fresh label, exactly like the old "New region" but targeting a new label. */
|
|
79195
|
+
newSegment(label) {
|
|
79196
|
+
this.commitRegion();
|
|
79197
|
+
this.setActiveSegment(label);
|
|
79198
|
+
}
|
|
79199
|
+
/** Delete a Segmentation: erase every voxel painted with its label from BOTH the
|
|
79200
|
+
* live scratch and the frozen (committed) volume, so the region disappears from
|
|
79201
|
+
* the view (otherwise orphaned label voxels would render with the wrong colour).
|
|
79202
|
+
* Goes via setRawData so the volume's version counter bumps (overlay repaints). */
|
|
79203
|
+
clearSegment(label) {
|
|
79204
|
+
const lbl = Math.round(label);
|
|
79205
|
+
const zeroOut = (v) => {
|
|
79206
|
+
if (!v)
|
|
79207
|
+
return;
|
|
79208
|
+
const raw = v.getRawData().slice();
|
|
79209
|
+
let changed = false;
|
|
79210
|
+
for (let i = 0; i < raw.length; i++) {
|
|
79211
|
+
if (raw[i] === lbl) {
|
|
79212
|
+
raw[i] = 0;
|
|
79213
|
+
changed = true;
|
|
79214
|
+
}
|
|
79215
|
+
}
|
|
79216
|
+
if (changed)
|
|
79217
|
+
v.setRawData(raw);
|
|
79218
|
+
};
|
|
79219
|
+
zeroOut(this.scratchVol);
|
|
79220
|
+
zeroOut(this.committedVol);
|
|
79055
79221
|
}
|
|
79056
|
-
getChannel() { return this.channel; }
|
|
79057
79222
|
getScratchVolume() { return this.scratchVol; }
|
|
79058
79223
|
/** Serialize the scratch volume as per-z-slice RLE (only NON-empty slices),
|
|
79059
79224
|
* for persisting to ai_generated_nii_LPS on the backend. Binary (any channel →
|
|
@@ -79095,6 +79260,50 @@ class AiAssistTool extends BaseTool {
|
|
|
79095
79260
|
}
|
|
79096
79261
|
return { axis: "z", width, height, slices: out };
|
|
79097
79262
|
}
|
|
79263
|
+
/** Per-SEGMENTATION serialization for the 3D build: one binary RLE per non-empty
|
|
79264
|
+
* z-slice PER label (no binarize-merge), so the backend writes a multi-label
|
|
79265
|
+
* NIfTI and colours the GLB per segmentation. Includes both live and frozen
|
|
79266
|
+
* regions (both live in scratchVol). */
|
|
79267
|
+
getScratchSegments() {
|
|
79268
|
+
if (!this.scratchVol)
|
|
79269
|
+
return null;
|
|
79270
|
+
const { depth } = this.scratchVol.getDimensions();
|
|
79271
|
+
const byLabel = new Map();
|
|
79272
|
+
let width = 0;
|
|
79273
|
+
let height = 0;
|
|
79274
|
+
for (let z = 0; z < depth; z++) {
|
|
79275
|
+
const { data, width: w, height: h } = this.scratchVol.getSliceUint8(z, "z");
|
|
79276
|
+
width = w;
|
|
79277
|
+
height = h;
|
|
79278
|
+
const labels = new Set();
|
|
79279
|
+
for (let i = 0; i < data.length; i++) {
|
|
79280
|
+
if (data[i])
|
|
79281
|
+
labels.add(data[i]);
|
|
79282
|
+
}
|
|
79283
|
+
for (const label of labels) {
|
|
79284
|
+
const runs = [];
|
|
79285
|
+
let value = 0; // always begin with a 0-run (matches backend rle_decode)
|
|
79286
|
+
let count = 0;
|
|
79287
|
+
for (let i = 0; i < data.length; i++) {
|
|
79288
|
+
const v = data[i] === label ? 1 : 0;
|
|
79289
|
+
if (v === value) {
|
|
79290
|
+
count++;
|
|
79291
|
+
}
|
|
79292
|
+
else {
|
|
79293
|
+
runs.push(count);
|
|
79294
|
+
value = v;
|
|
79295
|
+
count = 1;
|
|
79296
|
+
}
|
|
79297
|
+
}
|
|
79298
|
+
runs.push(count);
|
|
79299
|
+
if (!byLabel.has(label))
|
|
79300
|
+
byLabel.set(label, []);
|
|
79301
|
+
byLabel.get(label).push({ sliceIndex: z, rle: runs });
|
|
79302
|
+
}
|
|
79303
|
+
}
|
|
79304
|
+
const segments = [...byLabel.entries()].map(([label, slices]) => ({ label, slices }));
|
|
79305
|
+
return { axis: "z", width, height, segments };
|
|
79306
|
+
}
|
|
79098
79307
|
// ── Sandbox lifecycle ──────────────────────────────────────────────────────
|
|
79099
79308
|
/** Create (or reuse) the scratch volume sized to the layer grid and register
|
|
79100
79309
|
* it under maskData.volumes['aiScratch']. Snapshots the empty buffer. */
|
|
@@ -79115,14 +79324,10 @@ class AiAssistTool extends BaseTool {
|
|
|
79115
79324
|
this.scratchVol.clear();
|
|
79116
79325
|
}
|
|
79117
79326
|
volumes[AI_SCRATCH_LAYER] = this.scratchVol;
|
|
79118
|
-
//
|
|
79119
|
-
//
|
|
79120
|
-
//
|
|
79121
|
-
|
|
79122
|
-
const c = AI_MASK_CHANNEL_COLORS[ch];
|
|
79123
|
-
if (c)
|
|
79124
|
-
this.scratchVol.setChannelColor(ch, { r: c.r, g: c.g, b: c.b, a: c.a });
|
|
79125
|
-
}
|
|
79327
|
+
// Segmentation colours are now driven per-label from the app (store → useAiAssist
|
|
79328
|
+
// → setSegmentColor) right after enter, so the scratch can carry an arbitrary
|
|
79329
|
+
// number of independently-coloured segmentations. No fixed palette is applied
|
|
79330
|
+
// here. Scoped to THIS volume only — global/clinician palettes are untouched.
|
|
79126
79331
|
this.snapshotData = this.scratchVol.getRawData().slice();
|
|
79127
79332
|
this.committedVol = null; // nothing frozen at session start
|
|
79128
79333
|
this.resetPrompts();
|
|
@@ -79168,10 +79373,26 @@ class AiAssistTool extends BaseTool {
|
|
|
79168
79373
|
resetPrompts() {
|
|
79169
79374
|
this.points = [];
|
|
79170
79375
|
this.scribble = [];
|
|
79376
|
+
this.lasso = [];
|
|
79171
79377
|
this.box = undefined;
|
|
79172
79378
|
this.dragScreenStart = null;
|
|
79173
79379
|
this.dragScreen = null;
|
|
79174
79380
|
this.screenScribble = [];
|
|
79381
|
+
this.clearLassoEditing();
|
|
79382
|
+
}
|
|
79383
|
+
/** Drop the in-progress lasso vertices + undo/redo history (does not touch a
|
|
79384
|
+
* prediction already painted from a finished lasso). */
|
|
79385
|
+
clearLassoEditing() {
|
|
79386
|
+
this.lassoVerts = [];
|
|
79387
|
+
this.lassoUndo = [];
|
|
79388
|
+
this.lassoRedo = [];
|
|
79389
|
+
this.lassoHoverIdx = -1;
|
|
79390
|
+
this.notifyLasso();
|
|
79391
|
+
}
|
|
79392
|
+
/** Tell the app layer the lasso vertex set changed (drives the Finish button). */
|
|
79393
|
+
notifyLasso() {
|
|
79394
|
+
var _a;
|
|
79395
|
+
(_a = this.onLassoChange) === null || _a === void 0 ? void 0 : _a.call(this, this.lassoVerts.length, this.lassoVerts.length > 0);
|
|
79175
79396
|
}
|
|
79176
79397
|
// ── Coordinate mapping (all three views) ───────────────────────────────────
|
|
79177
79398
|
//
|
|
@@ -79232,14 +79453,53 @@ class AiAssistTool extends BaseTool {
|
|
|
79232
79453
|
return null;
|
|
79233
79454
|
return { vx, vy, w: sd.w, h: sd.h };
|
|
79234
79455
|
}
|
|
79456
|
+
/** Voxel-slice (vx,vy) → screen offset (px). Exact inverse of toVoxel, so
|
|
79457
|
+
* markers / lasso curves drawn from voxel coords track pan AND zoom each frame
|
|
79458
|
+
* (the freehand previews store screen px directly; persistent overlays don't). */
|
|
79459
|
+
voxelToScreen(vx, vy) {
|
|
79460
|
+
const sd = this.sliceDims();
|
|
79461
|
+
if (!sd)
|
|
79462
|
+
return null;
|
|
79463
|
+
const nrrd = this.ctx.nrrd_states;
|
|
79464
|
+
const img = nrrd.image;
|
|
79465
|
+
let hMM, vMM, flipV;
|
|
79466
|
+
switch (this.ctx.protectedData.axis) {
|
|
79467
|
+
case "z":
|
|
79468
|
+
hMM = img.nrrd_x_mm;
|
|
79469
|
+
vMM = img.nrrd_y_mm;
|
|
79470
|
+
flipV = false;
|
|
79471
|
+
break;
|
|
79472
|
+
case "y":
|
|
79473
|
+
hMM = img.nrrd_x_mm;
|
|
79474
|
+
vMM = img.nrrd_z_mm;
|
|
79475
|
+
flipV = true;
|
|
79476
|
+
break;
|
|
79477
|
+
case "x":
|
|
79478
|
+
hMM = img.nrrd_z_mm;
|
|
79479
|
+
vMM = img.nrrd_y_mm;
|
|
79480
|
+
flipV = false;
|
|
79481
|
+
break;
|
|
79482
|
+
default: return null;
|
|
79483
|
+
}
|
|
79484
|
+
const vyRaw = flipV ? sd.h - 1 - vy : vy;
|
|
79485
|
+
const x = (vx * hMM / sd.w) * nrrd.view.sizeFactor;
|
|
79486
|
+
const y = (vyRaw * vMM / sd.h) * nrrd.view.sizeFactor;
|
|
79487
|
+
return { x, y };
|
|
79488
|
+
}
|
|
79235
79489
|
// ── Pointer handlers (left button; right stays pan in EventRouter) ──────────
|
|
79236
79490
|
onPointerDown(e) {
|
|
79237
79491
|
const hit = this.toVoxel(e);
|
|
79238
79492
|
if (!hit)
|
|
79239
79493
|
return;
|
|
79240
79494
|
this.syncSlice();
|
|
79495
|
+
// Lasso is click-to-place vertices (NOT a drag) — own handler.
|
|
79496
|
+
if (this.promptTool === "lasso") {
|
|
79497
|
+
this.onLassoPointerDown(e, hit.vx, hit.vy);
|
|
79498
|
+
return;
|
|
79499
|
+
}
|
|
79241
79500
|
if (this.promptTool === "point") {
|
|
79242
79501
|
this.points.push({ x: hit.vx, y: hit.vy, label: this.polarity });
|
|
79502
|
+
this.hidePointMarkers = false; // show the new seed until its mask returns
|
|
79243
79503
|
this.emit();
|
|
79244
79504
|
}
|
|
79245
79505
|
else {
|
|
@@ -79255,9 +79515,43 @@ class AiAssistTool extends BaseTool {
|
|
|
79255
79515
|
}
|
|
79256
79516
|
}
|
|
79257
79517
|
}
|
|
79518
|
+
/** Lasso click: double-click finishes; click on a vertex deletes it; otherwise
|
|
79519
|
+
* add a vertex. Nothing is sent to the backend until finishLasso(). */
|
|
79520
|
+
onLassoPointerDown(e, vx, vy) {
|
|
79521
|
+
const now = Date.now();
|
|
79522
|
+
const dxl = e.offsetX - this.lastLassoDownPos.x;
|
|
79523
|
+
const dyl = e.offsetY - this.lastLassoDownPos.y;
|
|
79524
|
+
const isDbl = now - this.lastLassoDownTime < 300 &&
|
|
79525
|
+
dxl * dxl + dyl * dyl <= 36; // ~6px
|
|
79526
|
+
this.lastLassoDownTime = now;
|
|
79527
|
+
this.lastLassoDownPos = { x: e.offsetX, y: e.offsetY };
|
|
79528
|
+
// Double-click → finish (the 1st click of the pair already placed the vertex).
|
|
79529
|
+
if (isDbl && this.lassoVerts.length >= 3) {
|
|
79530
|
+
this.finishLasso();
|
|
79531
|
+
return;
|
|
79532
|
+
}
|
|
79533
|
+
// Click on an existing vertex → delete it (recompute the curve).
|
|
79534
|
+
const idx = this.lassoVertAt(e.offsetX, e.offsetY);
|
|
79535
|
+
if (idx >= 0) {
|
|
79536
|
+
this.pushLassoUndo();
|
|
79537
|
+
this.lassoVerts.splice(idx, 1);
|
|
79538
|
+
this.lassoHoverIdx = -1;
|
|
79539
|
+
this.notifyLasso();
|
|
79540
|
+
return;
|
|
79541
|
+
}
|
|
79542
|
+
// Otherwise add a new vertex.
|
|
79543
|
+
this.pushLassoUndo();
|
|
79544
|
+
this.lassoVerts.push({ vx, vy, sx: e.offsetX, sy: e.offsetY, label: this.polarity });
|
|
79545
|
+
this.notifyLasso();
|
|
79546
|
+
}
|
|
79258
79547
|
onPointerMove(e) {
|
|
79259
|
-
// Track hover for the scribble preview ring even when
|
|
79548
|
+
// Track hover for the scribble preview ring / point crosshair even when idle.
|
|
79260
79549
|
this.hoverScreen = { x: e.offsetX, y: e.offsetY };
|
|
79550
|
+
// Lasso: update which vertex (if any) is hovered, for the red ✕ delete affordance.
|
|
79551
|
+
if (this.promptTool === "lasso") {
|
|
79552
|
+
this.lassoHoverIdx = this.lassoVertAt(e.offsetX, e.offsetY);
|
|
79553
|
+
return; // lasso is click-based; no drag accumulation
|
|
79554
|
+
}
|
|
79261
79555
|
if (!this.dragging)
|
|
79262
79556
|
return;
|
|
79263
79557
|
// Track screen pos first so the live preview follows even slightly out of bounds.
|
|
@@ -79310,12 +79604,116 @@ class AiAssistTool extends BaseTool {
|
|
|
79310
79604
|
box: this.box,
|
|
79311
79605
|
scribble: this.scribble.length ? [...this.scribble] : undefined,
|
|
79312
79606
|
scribbleRadius: this.scribbleSize,
|
|
79607
|
+
lasso: this.lasso.length ? [...this.lasso] : undefined,
|
|
79313
79608
|
});
|
|
79314
79609
|
}
|
|
79610
|
+
// ── Lasso v2 (discrete vertices → smooth closed curve) ──────────────────────
|
|
79611
|
+
/** Index of the lasso vertex under (sx,sy) within the hit radius, else −1.
|
|
79612
|
+
* Uses live voxel→screen so it tracks pan/zoom. Topmost (latest) wins. */
|
|
79613
|
+
lassoVertAt(sx, sy) {
|
|
79614
|
+
var _a;
|
|
79615
|
+
const r2 = AiAssistTool.LASSO_HIT_R * AiAssistTool.LASSO_HIT_R;
|
|
79616
|
+
for (let i = this.lassoVerts.length - 1; i >= 0; i--) {
|
|
79617
|
+
const v = this.lassoVerts[i];
|
|
79618
|
+
const s = (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy };
|
|
79619
|
+
const dx = sx - s.x, dy = sy - s.y;
|
|
79620
|
+
if (dx * dx + dy * dy <= r2)
|
|
79621
|
+
return i;
|
|
79622
|
+
}
|
|
79623
|
+
return -1;
|
|
79624
|
+
}
|
|
79625
|
+
/** Snapshot the vertex list for undo (clears redo). */
|
|
79626
|
+
pushLassoUndo() {
|
|
79627
|
+
this.lassoUndo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79628
|
+
this.lassoRedo = [];
|
|
79629
|
+
if (this.lassoUndo.length > 100)
|
|
79630
|
+
this.lassoUndo.shift();
|
|
79631
|
+
}
|
|
79632
|
+
/** Public: undo the last add/delete of a lasso vertex (Ctrl+Z while editing). */
|
|
79633
|
+
lassoUndoAction() {
|
|
79634
|
+
if (!this.lassoUndo.length)
|
|
79635
|
+
return;
|
|
79636
|
+
this.lassoRedo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79637
|
+
this.lassoVerts = this.lassoUndo.pop();
|
|
79638
|
+
this.lassoHoverIdx = -1;
|
|
79639
|
+
this.notifyLasso();
|
|
79640
|
+
}
|
|
79641
|
+
/** Public: redo (Ctrl+Y / Ctrl+Shift+Z while editing). */
|
|
79642
|
+
lassoRedoAction() {
|
|
79643
|
+
if (!this.lassoRedo.length)
|
|
79644
|
+
return;
|
|
79645
|
+
this.lassoUndo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79646
|
+
this.lassoVerts = this.lassoRedo.pop();
|
|
79647
|
+
this.lassoHoverIdx = -1;
|
|
79648
|
+
this.notifyLasso();
|
|
79649
|
+
}
|
|
79650
|
+
/** Public: abandon the in-progress lasso (Esc). */
|
|
79651
|
+
cancelLasso() { this.clearLassoEditing(); }
|
|
79652
|
+
/** Public: true while the user is placing lasso vertices (≥1 vertex). */
|
|
79653
|
+
isLassoEditing() { return this.lassoVerts.length > 0; }
|
|
79654
|
+
/** Public: number of placed lasso vertices (drives the Finish button label/enable). */
|
|
79655
|
+
getLassoVertCount() { return this.lassoVerts.length; }
|
|
79656
|
+
/** Vertices ordered by polar angle around their centroid, so the closed curve is
|
|
79657
|
+
* a SIMPLE (non-self-intersecting) loop regardless of click order — otherwise
|
|
79658
|
+
* connecting points in click order + closing easily produces a bow-tie crossing.
|
|
79659
|
+
* Trade-off: deep concavities collapse to their star-shaped hull, which is fine
|
|
79660
|
+
* for a "rough loop around a blob". */
|
|
79661
|
+
orderedLassoVerts() {
|
|
79662
|
+
const n = this.lassoVerts.length;
|
|
79663
|
+
if (n < 3)
|
|
79664
|
+
return this.lassoVerts.slice();
|
|
79665
|
+
let cx = 0, cy = 0;
|
|
79666
|
+
for (const v of this.lassoVerts) {
|
|
79667
|
+
cx += v.vx;
|
|
79668
|
+
cy += v.vy;
|
|
79669
|
+
}
|
|
79670
|
+
cx /= n;
|
|
79671
|
+
cy /= n;
|
|
79672
|
+
return this.lassoVerts.slice().sort((a, b) => Math.atan2(a.vy - cy, a.vx - cx) - Math.atan2(b.vy - cy, b.vx - cx));
|
|
79673
|
+
}
|
|
79674
|
+
/** Public: close the curve, densify it, and send it to the backend as the lasso
|
|
79675
|
+
* contour. Needs ≥3 vertices (a real area). Then clears the editing state. */
|
|
79676
|
+
finishLasso() {
|
|
79677
|
+
if (this.lassoVerts.length < 3)
|
|
79678
|
+
return;
|
|
79679
|
+
// Order by angle (no self-crossing), then sample the SAME closed curve the user
|
|
79680
|
+
// saw → contour points (so the mask matches the curve, not a straight polygon).
|
|
79681
|
+
const contour = this.sampleClosedSpline(this.orderedLassoVerts().map((v) => ({ x: v.vx, y: v.vy })), 16);
|
|
79682
|
+
const label = this.lassoVerts[0].label; // a lasso carries one polarity (set at start)
|
|
79683
|
+
this.lasso = contour.map((p) => ({ x: Math.round(p.x), y: Math.round(p.y), label }));
|
|
79684
|
+
this.clearLassoEditing();
|
|
79685
|
+
this.emit();
|
|
79686
|
+
}
|
|
79687
|
+
/** Sample a smooth CLOSED Catmull-Rom spline through `pts` → denser polyline.
|
|
79688
|
+
* `segs` samples per control-point span. <3 pts → returns a copy unchanged. */
|
|
79689
|
+
sampleClosedSpline(pts, segs) {
|
|
79690
|
+
const n = pts.length;
|
|
79691
|
+
if (n < 3)
|
|
79692
|
+
return pts.slice();
|
|
79693
|
+
const at = (i) => pts[((i % n) + n) % n];
|
|
79694
|
+
const out = [];
|
|
79695
|
+
for (let i = 0; i < n; i++) {
|
|
79696
|
+
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
|
|
79697
|
+
for (let s = 0; s < segs; s++) {
|
|
79698
|
+
const t = s / segs, t2 = t * t, t3 = t2 * t;
|
|
79699
|
+
out.push({
|
|
79700
|
+
x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t +
|
|
79701
|
+
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
|
|
79702
|
+
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
|
|
79703
|
+
y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t +
|
|
79704
|
+
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
|
|
79705
|
+
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
|
|
79706
|
+
});
|
|
79707
|
+
}
|
|
79708
|
+
}
|
|
79709
|
+
return out;
|
|
79710
|
+
}
|
|
79315
79711
|
// ── Apply backend result → scratch volume ──────────────────────────────────
|
|
79316
79712
|
applyMask(result) {
|
|
79317
79713
|
if (!this.scratchVol)
|
|
79318
79714
|
return;
|
|
79715
|
+
// A prediction landed → hide the point seed markers; only the mask should show.
|
|
79716
|
+
this.hidePointMarkers = true;
|
|
79319
79717
|
// 3D result (engine B): write each slice across the covered span.
|
|
79320
79718
|
if (result.slices && result.sliceRange) {
|
|
79321
79719
|
const [lo] = result.sliceRange;
|
|
@@ -79345,7 +79743,7 @@ class AiAssistTool extends BaseTool {
|
|
|
79345
79743
|
pos += run;
|
|
79346
79744
|
value ^= 1;
|
|
79347
79745
|
}
|
|
79348
|
-
const ch = this.
|
|
79746
|
+
const ch = this.activeLabel;
|
|
79349
79747
|
try {
|
|
79350
79748
|
const out = this.scratchVol.getSliceUint8(sliceIndex, axis).data; // copy
|
|
79351
79749
|
// Frozen same-channel pixels for this slice (if any region was committed).
|
|
@@ -79418,10 +79816,7 @@ class AiAssistTool extends BaseTool {
|
|
|
79418
79816
|
}
|
|
79419
79817
|
targetCtx.restore();
|
|
79420
79818
|
}
|
|
79421
|
-
// Scribble brush-size preview ring (when NOT dragging)
|
|
79422
|
-
// the cursor whose radius = scribbleSize, so the user sees the brush size and
|
|
79423
|
-
// how the slider changes it (mirrors the paint brush's preview ring). Drawn
|
|
79424
|
-
// every frame from the live hover position by copper3d's render loop.
|
|
79819
|
+
// Scribble brush-size preview ring (when NOT dragging).
|
|
79425
79820
|
if (this.promptTool === "scribble" && !this.dragging && this.hoverScreen) {
|
|
79426
79821
|
targetCtx.save();
|
|
79427
79822
|
const fg = this.polarity === 1;
|
|
@@ -79434,8 +79829,105 @@ class AiAssistTool extends BaseTool {
|
|
|
79434
79829
|
targetCtx.stroke();
|
|
79435
79830
|
targetCtx.restore();
|
|
79436
79831
|
}
|
|
79832
|
+
// Point markers + hover crosshair so the clinician sees exactly where each seed
|
|
79833
|
+
// lands (and where the next click will go) before/while the prediction returns.
|
|
79834
|
+
if (this.promptTool === "point") {
|
|
79835
|
+
if (!this.hidePointMarkers) {
|
|
79836
|
+
for (const p of this.points) {
|
|
79837
|
+
const s = this.voxelToScreen(p.x, p.y);
|
|
79838
|
+
if (s)
|
|
79839
|
+
this.drawSeedMarker(targetCtx, s.x, s.y, p.label === 1);
|
|
79840
|
+
}
|
|
79841
|
+
}
|
|
79842
|
+
if (!this.dragging && this.hoverScreen) {
|
|
79843
|
+
targetCtx.save();
|
|
79844
|
+
targetCtx.strokeStyle = this.polarity === 1
|
|
79845
|
+
? "rgba(120,255,180,0.55)" : "rgba(255,120,120,0.55)";
|
|
79846
|
+
targetCtx.lineWidth = 1;
|
|
79847
|
+
const { x, y } = this.hoverScreen;
|
|
79848
|
+
targetCtx.beginPath();
|
|
79849
|
+
targetCtx.moveTo(x - 7, y);
|
|
79850
|
+
targetCtx.lineTo(x + 7, y);
|
|
79851
|
+
targetCtx.moveTo(x, y - 7);
|
|
79852
|
+
targetCtx.lineTo(x, y + 7);
|
|
79853
|
+
targetCtx.stroke();
|
|
79854
|
+
targetCtx.restore();
|
|
79855
|
+
}
|
|
79856
|
+
}
|
|
79857
|
+
// Lasso v2: smooth closed curve through the placed vertices (closed + filled
|
|
79858
|
+
// from ≥2 vertices), vertex dots, and a red ✕ on the hovered vertex.
|
|
79859
|
+
if (this.promptTool === "lasso" && this.lassoVerts.length > 0) {
|
|
79860
|
+
this.renderLassoOverlay(targetCtx);
|
|
79861
|
+
}
|
|
79437
79862
|
}
|
|
79438
|
-
|
|
79863
|
+
/** A seed marker (small filled dot + ring) for point prompts. */
|
|
79864
|
+
drawSeedMarker(ctx, x, y, fg) {
|
|
79865
|
+
ctx.save();
|
|
79866
|
+
const col = fg ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79867
|
+
ctx.fillStyle = col;
|
|
79868
|
+
ctx.strokeStyle = "rgba(0,0,0,0.55)";
|
|
79869
|
+
ctx.lineWidth = 1.5;
|
|
79870
|
+
ctx.beginPath();
|
|
79871
|
+
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
|
79872
|
+
ctx.fill();
|
|
79873
|
+
ctx.stroke();
|
|
79874
|
+
ctx.restore();
|
|
79875
|
+
}
|
|
79876
|
+
/** Render the lasso editing overlay (curve + fill + vertices + hover ✕). */
|
|
79877
|
+
renderLassoOverlay(ctx) {
|
|
79878
|
+
var _a;
|
|
79879
|
+
const fg = this.lassoVerts[0].label === 1;
|
|
79880
|
+
const stroke = fg ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79881
|
+
const fill = fg ? "rgba(120,255,180,0.14)" : "rgba(255,120,120,0.14)";
|
|
79882
|
+
// PATH: angularly-ordered vertices (simple, non-crossing loop) in screen space.
|
|
79883
|
+
const ov = this.orderedLassoVerts().map((v) => { var _a; return (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy }; });
|
|
79884
|
+
const path = ov.length >= 3 ? this.sampleClosedSpline(ov, 16) : ov;
|
|
79885
|
+
ctx.save();
|
|
79886
|
+
if (path.length >= 2) {
|
|
79887
|
+
ctx.beginPath();
|
|
79888
|
+
ctx.moveTo(path[0].x, path[0].y);
|
|
79889
|
+
for (let i = 1; i < path.length; i++)
|
|
79890
|
+
ctx.lineTo(path[i].x, path[i].y);
|
|
79891
|
+
ctx.closePath();
|
|
79892
|
+
ctx.fillStyle = fill;
|
|
79893
|
+
ctx.fill();
|
|
79894
|
+
ctx.lineCap = "round";
|
|
79895
|
+
ctx.lineJoin = "round";
|
|
79896
|
+
ctx.lineWidth = 1.75;
|
|
79897
|
+
ctx.strokeStyle = stroke;
|
|
79898
|
+
ctx.stroke();
|
|
79899
|
+
}
|
|
79900
|
+
// Vertex handles — drawn from the ORIGINAL vertex order so lassoHoverIdx (an
|
|
79901
|
+
// index into lassoVerts from the hit-test) marks the correct one for the ✕.
|
|
79902
|
+
for (let i = 0; i < this.lassoVerts.length; i++) {
|
|
79903
|
+
const v = this.lassoVerts[i];
|
|
79904
|
+
const p = (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy };
|
|
79905
|
+
if (i === this.lassoHoverIdx) {
|
|
79906
|
+
// Red ✕ — click to delete this vertex.
|
|
79907
|
+
ctx.strokeStyle = "rgba(255,90,90,0.98)";
|
|
79908
|
+
ctx.lineWidth = 2;
|
|
79909
|
+
ctx.beginPath();
|
|
79910
|
+
ctx.moveTo(p.x - 5, p.y - 5);
|
|
79911
|
+
ctx.lineTo(p.x + 5, p.y + 5);
|
|
79912
|
+
ctx.moveTo(p.x + 5, p.y - 5);
|
|
79913
|
+
ctx.lineTo(p.x - 5, p.y + 5);
|
|
79914
|
+
ctx.stroke();
|
|
79915
|
+
}
|
|
79916
|
+
else {
|
|
79917
|
+
ctx.fillStyle = stroke;
|
|
79918
|
+
ctx.strokeStyle = "rgba(0,0,0,0.55)";
|
|
79919
|
+
ctx.lineWidth = 1.25;
|
|
79920
|
+
ctx.beginPath();
|
|
79921
|
+
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
|
79922
|
+
ctx.fill();
|
|
79923
|
+
ctx.stroke();
|
|
79924
|
+
}
|
|
79925
|
+
}
|
|
79926
|
+
ctx.restore();
|
|
79927
|
+
}
|
|
79928
|
+
}
|
|
79929
|
+
/** Screen-pixel radius for hit-testing a hovered vertex. */
|
|
79930
|
+
AiAssistTool.LASSO_HIT_R = 8;
|
|
79439
79931
|
|
|
79440
79932
|
/**
|
|
79441
79933
|
* DrawToolCore — Tool orchestration and event routing.
|
|
@@ -82470,16 +82962,39 @@ class NrrdTools {
|
|
|
82470
82962
|
// — Driver methods called by the app-layer composable —
|
|
82471
82963
|
aiSetPromptTool(tool) { this.drawCore.aiAssistTool.setPromptTool(tool); }
|
|
82472
82964
|
aiSetPolarity(label) { this.drawCore.aiAssistTool.setPolarity(label); }
|
|
82473
|
-
/**
|
|
82474
|
-
|
|
82965
|
+
/** Select the active Segmentation by its label value (the AI paints into it). */
|
|
82966
|
+
aiSetActiveSegment(label) { this.drawCore.aiAssistTool.setActiveSegment(label); }
|
|
82967
|
+
/** Set a Segmentation's colour (its label's colorMap entry; 2D overlay repaints). */
|
|
82968
|
+
aiSetSegmentColor(label, color) {
|
|
82969
|
+
this.drawCore.aiAssistTool.setSegmentColor(label, color);
|
|
82970
|
+
}
|
|
82971
|
+
/** "New segmentation": freeze current regions + switch to a new label. */
|
|
82972
|
+
aiNewSegment(label) { this.drawCore.aiAssistTool.newSegment(label); }
|
|
82973
|
+
/** Delete a Segmentation: erase all its label's voxels from scratch + committed. */
|
|
82974
|
+
aiClearSegment(label) { this.drawCore.aiAssistTool.clearSegment(label); }
|
|
82475
82975
|
/** Set the scribble brush radius (px). */
|
|
82476
82976
|
aiSetScribbleSize(size) { this.drawCore.aiAssistTool.setScribbleSize(size); }
|
|
82477
82977
|
/** Register the callback invoked when a prompt gesture completes (app → backend). */
|
|
82478
82978
|
aiOnPrompt(cb) { this.drawCore.aiAssistTool.onPrompt = cb; }
|
|
82979
|
+
/** Register the callback fired when the lasso vertex set changes (drives the Finish button). */
|
|
82980
|
+
aiOnLassoChange(cb) { this.drawCore.aiAssistTool.onLassoChange = cb; }
|
|
82479
82981
|
/** Apply a backend mask result into the scratch volume (overlay repaints next frame). */
|
|
82480
82982
|
aiApplyMask(result) { this.drawCore.aiAssistTool.applyMask(result); }
|
|
82481
82983
|
/** Clear the in-progress prompt set (e.g. slice change). */
|
|
82482
82984
|
aiClearPrompts() { this.drawCore.aiAssistTool.resetPrompts(); }
|
|
82985
|
+
// — Lasso v2 (discrete-vertex closed-curve) controls —
|
|
82986
|
+
/** Close + send the in-progress lasso to the backend (needs ≥3 vertices). */
|
|
82987
|
+
aiFinishLasso() { this.drawCore.aiAssistTool.finishLasso(); }
|
|
82988
|
+
/** Undo the last lasso vertex add/delete. */
|
|
82989
|
+
aiLassoUndo() { this.drawCore.aiAssistTool.lassoUndoAction(); }
|
|
82990
|
+
/** Redo the last undone lasso vertex change. */
|
|
82991
|
+
aiLassoRedo() { this.drawCore.aiAssistTool.lassoRedoAction(); }
|
|
82992
|
+
/** Abandon the in-progress lasso (Esc). */
|
|
82993
|
+
aiCancelLasso() { this.drawCore.aiAssistTool.cancelLasso(); }
|
|
82994
|
+
/** True while the user is placing lasso vertices. */
|
|
82995
|
+
aiIsLassoEditing() { return this.drawCore.aiAssistTool.isLassoEditing(); }
|
|
82996
|
+
/** Number of placed lasso vertices (drives the Finish button). */
|
|
82997
|
+
aiLassoVertCount() { return this.drawCore.aiAssistTool.getLassoVertCount(); }
|
|
82483
82998
|
/** "New region": freeze current regions (they persist) + start a fresh prompt set. */
|
|
82484
82999
|
aiCommitRegion() { this.drawCore.aiAssistTool.commitRegion(); }
|
|
82485
83000
|
/** Discard all AI scratch painting since enter (sandbox discard). */
|
|
@@ -82490,6 +83005,11 @@ class NrrdTools {
|
|
|
82490
83005
|
aiGetScratchSlices() {
|
|
82491
83006
|
return this.drawCore.aiAssistTool.getScratchSlices();
|
|
82492
83007
|
}
|
|
83008
|
+
/** Per-segmentation serialization (one binary RLE per label per z-slice) for the
|
|
83009
|
+
* multi-label / per-colour 3D build. */
|
|
83010
|
+
aiGetScratchSegments() {
|
|
83011
|
+
return this.drawCore.aiAssistTool.getScratchSegments();
|
|
83012
|
+
}
|
|
82493
83013
|
/**
|
|
82494
83014
|
* Merge the AI scratch into a target layer as a single undoable group (sandbox
|
|
82495
83015
|
* merge — best-practice: non-destructive + one Ctrl+Z). The scratch's channel
|
|
@@ -82892,7 +83412,14 @@ function evaluateElement(element, weights) {
|
|
|
82892
83412
|
}
|
|
82893
83413
|
|
|
82894
83414
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
82895
|
-
|
|
82896
|
-
|
|
83415
|
+
// "v3.6.5" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version
|
|
83416
|
+
const REVISION = "v3.6.5";
|
|
83417
|
+
// Expose on global so the version can be read in a production browser console via window.__COPPER3D_VERSION__
|
|
83418
|
+
if (typeof window !== "undefined") {
|
|
83419
|
+
window.__COPPER3D_VERSION__ = REVISION;
|
|
83420
|
+
}
|
|
83421
|
+
if (typeof console !== "undefined") {
|
|
83422
|
+
console.log(`%cCopper3D Visualisation %c${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
|
|
83423
|
+
}
|
|
82897
83424
|
|
|
82898
83425
|
export { AI_CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, SurfaceAnnotator, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };
|