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.umd.js
CHANGED
|
@@ -40873,9 +40873,9 @@ void main() {
|
|
|
40873
40873
|
}
|
|
40874
40874
|
}
|
|
40875
40875
|
|
|
40876
|
-
var vert = "
|
|
40876
|
+
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";
|
|
40877
40877
|
|
|
40878
|
-
var frag = "precision mediump float;\n#
|
|
40878
|
+
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";
|
|
40879
40879
|
|
|
40880
40880
|
function createBackground(opt) {
|
|
40881
40881
|
opt = opt || {};
|
|
@@ -41403,9 +41403,9 @@ void main() {
|
|
|
41403
41403
|
};
|
|
41404
41404
|
}
|
|
41405
41405
|
|
|
41406
|
-
var vert_2d = "
|
|
41406
|
+
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";
|
|
41407
41407
|
|
|
41408
|
-
var frag_2d = "precision highp float
|
|
41408
|
+
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}";
|
|
41409
41409
|
|
|
41410
41410
|
let planeWidth = 80;
|
|
41411
41411
|
let planeHeight = 80;
|
|
@@ -46336,7 +46336,7 @@ void main() {
|
|
|
46336
46336
|
mesh.position.y += mesh.position.y - center.y;
|
|
46337
46337
|
mesh.position.z += mesh.position.z - center.z;
|
|
46338
46338
|
const boundingBox = new Box3().setFromObject(mesh);
|
|
46339
|
-
//
|
|
46339
|
+
// Get the size of the bounding box
|
|
46340
46340
|
const size_ = new Vector3();
|
|
46341
46341
|
boundingBox.getSize(size_);
|
|
46342
46342
|
scene.add(mesh);
|
|
@@ -58686,7 +58686,7 @@ void main() {
|
|
|
58686
58686
|
const _EPS = 0.000001;
|
|
58687
58687
|
const _STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
|
|
58688
58688
|
|
|
58689
|
-
const _v2 = new Vector2();
|
|
58689
|
+
const _v2$1 = new Vector2();
|
|
58690
58690
|
const _mouseChange = new Vector2();
|
|
58691
58691
|
const _objectUp = new Vector3();
|
|
58692
58692
|
const _pan = new Vector3();
|
|
@@ -59082,23 +59082,23 @@ void main() {
|
|
|
59082
59082
|
|
|
59083
59083
|
_getMouseOnScreen( pageX, pageY ) {
|
|
59084
59084
|
|
|
59085
|
-
_v2.set(
|
|
59085
|
+
_v2$1.set(
|
|
59086
59086
|
( pageX - this.screen.left ) / this.screen.width,
|
|
59087
59087
|
( pageY - this.screen.top ) / this.screen.height
|
|
59088
59088
|
);
|
|
59089
59089
|
|
|
59090
|
-
return _v2;
|
|
59090
|
+
return _v2$1;
|
|
59091
59091
|
|
|
59092
59092
|
}
|
|
59093
59093
|
|
|
59094
59094
|
_getMouseOnCircle( pageX, pageY ) {
|
|
59095
59095
|
|
|
59096
|
-
_v2.set(
|
|
59096
|
+
_v2$1.set(
|
|
59097
59097
|
( ( pageX - this.screen.width * 0.5 - this.screen.left ) / ( this.screen.width * 0.5 ) ),
|
|
59098
59098
|
( ( this.screen.height + 2 * ( this.screen.top - pageY ) ) / this.screen.width ) // screen.width intentional
|
|
59099
59099
|
);
|
|
59100
59100
|
|
|
59101
|
-
return _v2;
|
|
59101
|
+
return _v2$1;
|
|
59102
59102
|
|
|
59103
59103
|
}
|
|
59104
59104
|
|
|
@@ -59519,7 +59519,7 @@ void main() {
|
|
|
59519
59519
|
}
|
|
59520
59520
|
|
|
59521
59521
|
const _inv = new Matrix4();
|
|
59522
|
-
/**
|
|
59522
|
+
/** World-space hit point → local vertex (position via worldToLocal, normal via inverse transform). */
|
|
59523
59523
|
function worldHitToLocalVertex(h, mesh) {
|
|
59524
59524
|
const lp = mesh.worldToLocal(h.point.clone());
|
|
59525
59525
|
_inv.copy(mesh.matrixWorld).invert();
|
|
@@ -59534,20 +59534,80 @@ void main() {
|
|
|
59534
59534
|
faceIndex: h.faceIndex,
|
|
59535
59535
|
};
|
|
59536
59536
|
}
|
|
59537
|
-
const _nm = new Matrix3();
|
|
59538
|
-
/**
|
|
59537
|
+
const _nm$1 = new Matrix3();
|
|
59538
|
+
/** Local vertex → world position + world normal (for rendering). */
|
|
59539
59539
|
function localVertexToWorld(v, mesh) {
|
|
59540
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();
|
|
59541
|
+
_nm$1.getNormalMatrix(mesh.matrixWorld);
|
|
59542
|
+
const n = new Vector3(v.nx, v.ny, v.nz).applyMatrix3(_nm$1).normalize();
|
|
59543
59543
|
return { p, n };
|
|
59544
59544
|
}
|
|
59545
59545
|
|
|
59546
59546
|
const ray = new Raycaster();
|
|
59547
59547
|
const ndc = new Vector2();
|
|
59548
|
+
// scratch objects for the smooth-normal interpolation (avoid per-call allocation)
|
|
59549
|
+
const _nm = new Matrix3();
|
|
59550
|
+
const _lp = new Vector3();
|
|
59551
|
+
const _pa = new Vector3();
|
|
59552
|
+
const _pb = new Vector3();
|
|
59553
|
+
const _pc = new Vector3();
|
|
59554
|
+
const _na = new Vector3();
|
|
59555
|
+
const _nb = new Vector3();
|
|
59556
|
+
const _nc = new Vector3();
|
|
59557
|
+
const _v0 = new Vector3();
|
|
59558
|
+
const _v1 = new Vector3();
|
|
59559
|
+
const _v2 = new Vector3();
|
|
59560
|
+
/**
|
|
59561
|
+
* Interpolate a "smooth per-vertex normal" across the hit triangle using barycentric coordinates,
|
|
59562
|
+
* instead of using the per-face normal.
|
|
59563
|
+
*
|
|
59564
|
+
* Per-face normals on voxel / marching-cubes meshes are axis-aligned stair directions (±x/±y/±z);
|
|
59565
|
+
* using them to push the annotation line outward along the normal shoves points toward neighboring
|
|
59566
|
+
* stairs rather than cleanly "outward", making the line dip in and out on bumpy surfaces and get
|
|
59567
|
+
* occluded by the steps. A smooth normal varies continuously along the surface, so the outward
|
|
59568
|
+
* direction is stable and consistent and the line can float smoothly above the relief. Returns false
|
|
59569
|
+
* when interpolation isn't possible (caller falls back to the face normal).
|
|
59570
|
+
*/
|
|
59571
|
+
function interpolateLocalNormal(geom, face, localPoint, out) {
|
|
59572
|
+
const posAttr = geom.getAttribute("position");
|
|
59573
|
+
const norAttr = geom.getAttribute("normal");
|
|
59574
|
+
if (!posAttr || !norAttr)
|
|
59575
|
+
return false;
|
|
59576
|
+
_pa.fromBufferAttribute(posAttr, face.a);
|
|
59577
|
+
_pb.fromBufferAttribute(posAttr, face.b);
|
|
59578
|
+
_pc.fromBufferAttribute(posAttr, face.c);
|
|
59579
|
+
// Barycentric coordinates (local space): u/v/w
|
|
59580
|
+
_v0.subVectors(_pb, _pa);
|
|
59581
|
+
_v1.subVectors(_pc, _pa);
|
|
59582
|
+
_v2.subVectors(localPoint, _pa);
|
|
59583
|
+
const d00 = _v0.dot(_v0);
|
|
59584
|
+
const d01 = _v0.dot(_v1);
|
|
59585
|
+
const d11 = _v1.dot(_v1);
|
|
59586
|
+
const d20 = _v2.dot(_v0);
|
|
59587
|
+
const d21 = _v2.dot(_v1);
|
|
59588
|
+
const denom = d00 * d11 - d01 * d01;
|
|
59589
|
+
if (Math.abs(denom) < 1e-12)
|
|
59590
|
+
return false;
|
|
59591
|
+
const v = (d11 * d20 - d01 * d21) / denom;
|
|
59592
|
+
const w = (d00 * d21 - d01 * d20) / denom;
|
|
59593
|
+
const u = 1 - v - w;
|
|
59594
|
+
_na.fromBufferAttribute(norAttr, face.a);
|
|
59595
|
+
_nb.fromBufferAttribute(norAttr, face.b);
|
|
59596
|
+
_nc.fromBufferAttribute(norAttr, face.c);
|
|
59597
|
+
out
|
|
59598
|
+
.set(0, 0, 0)
|
|
59599
|
+
.addScaledVector(_na, u)
|
|
59600
|
+
.addScaledVector(_nb, v)
|
|
59601
|
+
.addScaledVector(_nc, w);
|
|
59602
|
+
if (out.lengthSq() < 1e-12)
|
|
59603
|
+
return false;
|
|
59604
|
+
out.normalize();
|
|
59605
|
+
return true;
|
|
59606
|
+
}
|
|
59548
59607
|
/**
|
|
59549
|
-
*
|
|
59550
|
-
*
|
|
59608
|
+
* Project screen coordinates (clientX/Y) onto the mesh surface, returning the hit point,
|
|
59609
|
+
* world-space normal, and faceIndex. Returns null on a miss. Local implementation that does not
|
|
59610
|
+
* depend on Copper3D's unexported raycast internals.
|
|
59551
59611
|
*/
|
|
59552
59612
|
function raycastSurface(camera, container, mesh, clientX, clientY) {
|
|
59553
59613
|
var _a;
|
|
@@ -59562,19 +59622,29 @@ void main() {
|
|
|
59562
59622
|
const point = h.point.clone();
|
|
59563
59623
|
const normal = new Vector3(0, 0, 1);
|
|
59564
59624
|
if (h.face) {
|
|
59565
|
-
normal
|
|
59566
|
-
|
|
59567
|
-
|
|
59625
|
+
// Prefer a smooth interpolated normal (stable outward direction on bumpy/voxel meshes);
|
|
59626
|
+
// fall back to the flat face normal if the geometry has no normal attribute.
|
|
59627
|
+
_lp.copy(h.point);
|
|
59628
|
+
mesh.worldToLocal(_lp);
|
|
59629
|
+
if (!interpolateLocalNormal(mesh.geometry, h.face, _lp, normal)) {
|
|
59630
|
+
normal.copy(h.face.normal);
|
|
59631
|
+
}
|
|
59632
|
+
_nm.getNormalMatrix(mesh.matrixWorld);
|
|
59633
|
+
normal.applyMatrix3(_nm).normalize();
|
|
59568
59634
|
}
|
|
59569
59635
|
return { point, normal, faceIndex: (_a = h.faceIndex) !== null && _a !== void 0 ? _a : -1 };
|
|
59570
59636
|
}
|
|
59571
59637
|
|
|
59572
59638
|
/**
|
|
59573
|
-
*
|
|
59574
|
-
*
|
|
59639
|
+
* Create a marker sphere (fiducial) at a local vertex. After local→world, nudge it
|
|
59640
|
+
* outward along the world normal so it doesn't sink halfway into the surface.
|
|
59641
|
+
* radius is passed in by the caller, scaled to the model bbox.
|
|
59575
59642
|
*/
|
|
59576
59643
|
function makePointMarker(v, mesh, color, radius) {
|
|
59577
59644
|
const geo = new SphereGeometry(radius, 16, 16);
|
|
59645
|
+
// depthTest ON so markers are occluded by the model when they are on the far side (no
|
|
59646
|
+
// see-through), consistent with the contour lines. Markers are volumetric spheres lifted off
|
|
59647
|
+
// the surface, so they still show on the visible side without z-fighting.
|
|
59578
59648
|
const mat = new MeshBasicMaterial({ color });
|
|
59579
59649
|
const m = new Mesh(geo, mat);
|
|
59580
59650
|
const { p, n } = localVertexToWorld(v, mesh);
|
|
@@ -59584,15 +59654,18 @@ void main() {
|
|
|
59584
59654
|
}
|
|
59585
59655
|
|
|
59586
59656
|
/**
|
|
59587
|
-
*
|
|
59588
|
-
* pointermove
|
|
59589
|
-
*
|
|
59657
|
+
* State machine for one stroke in mode A (freehand).
|
|
59658
|
+
* On pointermove it keeps calling addSample; samples whose world distance from the previous
|
|
59659
|
+
* sample is < minGap are dropped to avoid over-density.
|
|
59660
|
+
* Vertices are stored in local space (the mesh converts the world hit point to local).
|
|
59590
59661
|
*/
|
|
59591
59662
|
class StrokeContour {
|
|
59592
59663
|
/**
|
|
59593
|
-
* @param minGap
|
|
59594
|
-
* @param maxJump
|
|
59595
|
-
*
|
|
59664
|
+
* @param minGap minimum sample spacing (world space), debounce
|
|
59665
|
+
* @param maxJump jump threshold (world space): drop the sample when distance > maxJump and the
|
|
59666
|
+
* normal flips sharply, to prevent adjacent points landing at different depths
|
|
59667
|
+
* (and the straight segment cutting through the model) when the stroke grazes a
|
|
59668
|
+
* groove or contour edge.
|
|
59596
59669
|
*/
|
|
59597
59670
|
constructor(minGap, maxJump, mesh) {
|
|
59598
59671
|
this.minGap = minGap;
|
|
@@ -59611,10 +59684,12 @@ void main() {
|
|
|
59611
59684
|
if (this.has) {
|
|
59612
59685
|
const d = hit.point.distanceTo(this.last);
|
|
59613
59686
|
if (d < this.minGap)
|
|
59614
|
-
return; //
|
|
59615
|
-
//
|
|
59616
|
-
//
|
|
59617
|
-
//
|
|
59687
|
+
return; // too close, debounce
|
|
59688
|
+
// Jump rejection: distance suddenly large AND normal flips sharply (>~75°) → likely jumped to
|
|
59689
|
+
// the back/far side, so drop it, otherwise the straight segment between adjacent points cuts
|
|
59690
|
+
// through the model interior. Only reject when both conditions hold:
|
|
59691
|
+
// fast drawing (large distance, similar normals) isn't falsely dropped, and drawing over a
|
|
59692
|
+
// sharp edge (normal changes, distance small) isn't falsely dropped either.
|
|
59618
59693
|
if (d > this.maxJump && hit.normal.dot(this.lastNormal) < 0.25)
|
|
59619
59694
|
return;
|
|
59620
59695
|
}
|
|
@@ -60912,9 +60987,10 @@ void main() {
|
|
|
60912
60987
|
}
|
|
60913
60988
|
|
|
60914
60989
|
/**
|
|
60915
|
-
*
|
|
60916
|
-
*
|
|
60917
|
-
*
|
|
60990
|
+
* Flatten a sequence of annotation vertices (local) into the world [x,y,z, ...] that Line2 needs.
|
|
60991
|
+
* Each point goes local→world, then is pushed outward along the world normal by epsilon to avoid
|
|
60992
|
+
* z-fighting with the model surface.
|
|
60993
|
+
* When closed, the first point is appended to the end to form a ring.
|
|
60918
60994
|
*/
|
|
60919
60995
|
function flatten(verts, closed, epsilon, mesh) {
|
|
60920
60996
|
const pts = [];
|
|
@@ -60930,27 +61006,31 @@ void main() {
|
|
|
60930
61006
|
function makeContourLine(verts, color, closed, container, epsilon, mesh) {
|
|
60931
61007
|
const geo = new LineGeometry();
|
|
60932
61008
|
const pos = flatten(verts, closed, epsilon, mesh);
|
|
60933
|
-
// Line2
|
|
61009
|
+
// Line2 needs at least 2 points; when there aren't enough, give a degenerate placeholder that a later update fills in.
|
|
60934
61010
|
geo.setPositions(pos.length >= 6 ? pos : [0, 0, 0, 0, 0, 0]);
|
|
60935
61011
|
const mat = new LineMaterial({
|
|
60936
61012
|
color: new Color$1(color).getHex(),
|
|
60937
|
-
linewidth: 3, //
|
|
61013
|
+
linewidth: 3, // width in pixels (requires resolution to be set)
|
|
61014
|
+
// depthTest stays ON so the model correctly occludes the line when it is on the far side
|
|
61015
|
+
// (no see-through). The host applies polygonOffset to the SURFACE material so the line still
|
|
61016
|
+
// wins where it lies coplanar with the surface (no z-fighting / sinking on bumpy meshes).
|
|
60938
61017
|
});
|
|
60939
61018
|
mat.resolution.set(container.clientWidth, container.clientHeight);
|
|
60940
61019
|
const line = new Line2(geo, mat);
|
|
60941
61020
|
line.computeLineDistances();
|
|
60942
61021
|
line.renderOrder = 998;
|
|
60943
|
-
//
|
|
60944
|
-
//
|
|
61022
|
+
// Important: Line2's bounding sphere stays at the initial point, so once the geometry grows by
|
|
61023
|
+
// dragging, the real geometry gets wrongly culled. Disable frustum culling so the line always renders.
|
|
60945
61024
|
line.frustumCulled = false;
|
|
60946
61025
|
return line;
|
|
60947
61026
|
}
|
|
60948
61027
|
function updateContourLine(line, verts, closed, epsilon, mesh) {
|
|
60949
61028
|
const pos = flatten(verts, closed, epsilon, mesh);
|
|
60950
61029
|
if (pos.length < 6)
|
|
60951
|
-
return; // Line2
|
|
60952
|
-
//
|
|
60953
|
-
//
|
|
61030
|
+
return; // Line2 needs >=2 points
|
|
61031
|
+
// Important: calling setPositions on the existing geometry to grow it only renders the first
|
|
61032
|
+
// segment (a known pitfall). Instead, rebuild and swap in a new LineGeometry each time so the
|
|
61033
|
+
// instance count is correct and every segment renders.
|
|
60954
61034
|
const geo = new LineGeometry();
|
|
60955
61035
|
geo.setPositions(pos);
|
|
60956
61036
|
const old = line.geometry;
|
|
@@ -60958,20 +61038,22 @@ void main() {
|
|
|
60958
61038
|
old.dispose();
|
|
60959
61039
|
line.computeLineDistances();
|
|
60960
61040
|
}
|
|
60961
|
-
/**
|
|
61041
|
+
/** Change the color of an existing contour line. */
|
|
60962
61042
|
function setContourColor(line, color) {
|
|
60963
61043
|
line.material.color.set(color);
|
|
60964
61044
|
}
|
|
60965
61045
|
|
|
60966
61046
|
/**
|
|
60967
|
-
*
|
|
60968
|
-
*
|
|
61047
|
+
* Build a vertex adjacency graph from a BufferGeometry's index, used for the shortest path in
|
|
61048
|
+
* mode B (geodesic). Vertex coordinates are stored in local space; query points must be converted
|
|
61049
|
+
* to local first (mesh.worldToLocal).
|
|
60969
61050
|
*
|
|
60970
|
-
*
|
|
60971
|
-
*
|
|
61051
|
+
* Note: the geometry must be indexed. modelLoader welds duplicate vertices with mergeVertices so
|
|
61052
|
+
* that vertices at the same surface position are shared, otherwise the adjacency graph won't be connected.
|
|
60972
61053
|
*
|
|
60973
|
-
*
|
|
60974
|
-
*
|
|
61054
|
+
* Performance: O(V²) Dijkstra + O(V) nearest-vertex lookup. Acceptable for a single click on tens
|
|
61055
|
+
* of thousands of vertices; if too large it can later be sped up with a binary heap + spatial grid
|
|
61056
|
+
* (left as a future upgrade).
|
|
60975
61057
|
*/
|
|
60976
61058
|
class MeshGraph {
|
|
60977
61059
|
constructor(geometry) {
|
|
@@ -60999,7 +61081,7 @@ void main() {
|
|
|
60999
61081
|
}
|
|
61000
61082
|
}
|
|
61001
61083
|
else {
|
|
61002
|
-
//
|
|
61084
|
+
// Non-indexed fallback (poor connectivity, only to avoid crashing).
|
|
61003
61085
|
for (let i = 0; i + 2 < this.vertexCount; i += 3) {
|
|
61004
61086
|
addEdge(i, i + 1);
|
|
61005
61087
|
addEdge(i + 1, i + 2);
|
|
@@ -61016,7 +61098,7 @@ void main() {
|
|
|
61016
61098
|
pz(i) {
|
|
61017
61099
|
return this.positions[i * 3 + 2];
|
|
61018
61100
|
}
|
|
61019
|
-
/**
|
|
61101
|
+
/** Nearest vertex (pass in local coordinates). */
|
|
61020
61102
|
nearestVertex(localPoint) {
|
|
61021
61103
|
let best = -1;
|
|
61022
61104
|
let bestD = Infinity;
|
|
@@ -61033,8 +61115,8 @@ void main() {
|
|
|
61033
61115
|
return best;
|
|
61034
61116
|
}
|
|
61035
61117
|
/**
|
|
61036
|
-
*
|
|
61037
|
-
*
|
|
61118
|
+
* Shortest path between two vertices (sequence of vertex indices including endpoints).
|
|
61119
|
+
* Binary-heap Dijkstra, O(E log V). Falls back to [start,end] when disconnected.
|
|
61038
61120
|
*/
|
|
61039
61121
|
shortestPath(startV, endV) {
|
|
61040
61122
|
if (startV === endV)
|
|
@@ -61091,7 +61173,7 @@ void main() {
|
|
|
61091
61173
|
const nm = new Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
61092
61174
|
return n.applyMatrix3(nm).normalize();
|
|
61093
61175
|
}
|
|
61094
|
-
/**
|
|
61176
|
+
/** Vertex local coordinates + local normal (the graph geometry is already local space). */
|
|
61095
61177
|
vertexLocal(i) {
|
|
61096
61178
|
return {
|
|
61097
61179
|
x: this.px(i),
|
|
@@ -61104,7 +61186,7 @@ void main() {
|
|
|
61104
61186
|
};
|
|
61105
61187
|
}
|
|
61106
61188
|
}
|
|
61107
|
-
/**
|
|
61189
|
+
/** Minimal binary min-heap (ordered by priority, stores vertex indices). Used for Dijkstra. */
|
|
61108
61190
|
class MinHeap {
|
|
61109
61191
|
constructor() {
|
|
61110
61192
|
this.ids = [];
|
|
@@ -61161,22 +61243,23 @@ void main() {
|
|
|
61161
61243
|
}
|
|
61162
61244
|
|
|
61163
61245
|
/**
|
|
61164
|
-
*
|
|
61165
|
-
*
|
|
61246
|
+
* State machine for mode B (geodesic): click anchors one by one, find the shortest path between
|
|
61247
|
+
* adjacent anchors with MeshGraph, and stitch them into a polyline that hugs the surface.
|
|
61248
|
+
* Enter finishes and can close the loop (computing one more segment between first and last).
|
|
61166
61249
|
*/
|
|
61167
61250
|
class GeodesicContour {
|
|
61168
61251
|
constructor(graph, mesh) {
|
|
61169
61252
|
this.graph = graph;
|
|
61170
61253
|
this.mesh = mesh;
|
|
61171
|
-
this.anchors = []; //
|
|
61172
|
-
this.segments = []; //
|
|
61173
|
-
this.history = []; //
|
|
61254
|
+
this.anchors = []; // vertex indices
|
|
61255
|
+
this.segments = []; // path between adjacent anchors (endpoints included)
|
|
61256
|
+
this.history = []; // anchor snapshot before each add/remove, for undo
|
|
61174
61257
|
}
|
|
61175
|
-
/**
|
|
61258
|
+
/** Save a snapshot before any anchor change (the vertex-index array is tiny, so snapshot cost is negligible). */
|
|
61176
61259
|
snapshot() {
|
|
61177
61260
|
this.history.push(this.anchors.slice());
|
|
61178
61261
|
}
|
|
61179
|
-
/**
|
|
61262
|
+
/** Given a local hit point, snap to the nearest vertex and compute the path to the previous anchor. */
|
|
61180
61263
|
addAnchor(localHitPoint) {
|
|
61181
61264
|
const v = this.graph.nearestVertex(localHitPoint);
|
|
61182
61265
|
this.snapshot();
|
|
@@ -61186,7 +61269,7 @@ void main() {
|
|
|
61186
61269
|
}
|
|
61187
61270
|
this.anchors.push(v);
|
|
61188
61271
|
}
|
|
61189
|
-
/**
|
|
61272
|
+
/** Remove the anchor at the given index and recompute adjacent segments (allows canceling any middle point). */
|
|
61190
61273
|
removeAnchorAt(index) {
|
|
61191
61274
|
if (index < 0 || index >= this.anchors.length)
|
|
61192
61275
|
return;
|
|
@@ -61194,11 +61277,11 @@ void main() {
|
|
|
61194
61277
|
this.anchors.splice(index, 1);
|
|
61195
61278
|
this.rebuildSegments();
|
|
61196
61279
|
}
|
|
61197
|
-
/**
|
|
61280
|
+
/** Whether there is still an editable change to undo (add/remove point). */
|
|
61198
61281
|
canUndo() {
|
|
61199
61282
|
return this.history.length > 0;
|
|
61200
61283
|
}
|
|
61201
|
-
/**
|
|
61284
|
+
/** Undo the last anchor edit (add → remove it; remove → restore it). Returns false when no history. */
|
|
61202
61285
|
undoEdit() {
|
|
61203
61286
|
const prev = this.history.pop();
|
|
61204
61287
|
if (!prev)
|
|
@@ -61207,21 +61290,21 @@ void main() {
|
|
|
61207
61290
|
this.rebuildSegments();
|
|
61208
61291
|
return true;
|
|
61209
61292
|
}
|
|
61210
|
-
/**
|
|
61293
|
+
/** Recompute all adjacent-segment paths from the current anchor sequence. */
|
|
61211
61294
|
rebuildSegments() {
|
|
61212
61295
|
this.segments = [];
|
|
61213
61296
|
for (let i = 1; i < this.anchors.length; i++) {
|
|
61214
61297
|
this.segments.push(this.graph.shortestPath(this.anchors[i - 1], this.anchors[i]));
|
|
61215
61298
|
}
|
|
61216
61299
|
}
|
|
61217
|
-
/**
|
|
61300
|
+
/** Local vertex of each anchor (used to draw visible anchor markers). */
|
|
61218
61301
|
getAnchorLocals() {
|
|
61219
61302
|
return this.anchors.map((i) => this.graph.vertexLocal(i));
|
|
61220
61303
|
}
|
|
61221
61304
|
get anchorCount() {
|
|
61222
61305
|
return this.anchors.length;
|
|
61223
61306
|
}
|
|
61224
|
-
/**
|
|
61307
|
+
/** Stitch all path vertices (world) + normals into a polyline; when closed, add one first-to-last segment. */
|
|
61225
61308
|
buildVertices(closed) {
|
|
61226
61309
|
const segs = this.segments.slice();
|
|
61227
61310
|
if (closed && this.anchors.length > 2) {
|
|
@@ -61229,22 +61312,22 @@ void main() {
|
|
|
61229
61312
|
}
|
|
61230
61313
|
const idxPath = [];
|
|
61231
61314
|
segs.forEach((s, si) => {
|
|
61232
|
-
const start = si === 0 ? 0 : 1; //
|
|
61315
|
+
const start = si === 0 ? 0 : 1; // dedupe the endpoint shared between adjacent segments
|
|
61233
61316
|
for (let k = start; k < s.length; k++)
|
|
61234
61317
|
idxPath.push(s[k]);
|
|
61235
61318
|
});
|
|
61236
61319
|
if (idxPath.length === 0 && this.anchors.length === 1) {
|
|
61237
61320
|
idxPath.push(this.anchors[0]);
|
|
61238
61321
|
}
|
|
61239
|
-
//
|
|
61322
|
+
// The graph geometry is already local space, so emit local vertices directly (world is derived at render time).
|
|
61240
61323
|
return idxPath.map((i) => this.graph.vertexLocal(i));
|
|
61241
61324
|
}
|
|
61242
61325
|
}
|
|
61243
61326
|
|
|
61244
61327
|
/**
|
|
61245
|
-
*
|
|
61246
|
-
*
|
|
61247
|
-
*
|
|
61328
|
+
* Annotation data model + undo stack + export. Pure data, no three / DOM dependencies.
|
|
61329
|
+
* Undo only records two atomic operations, add / remove (enough for "undo last add / restore delete").
|
|
61330
|
+
* Notifies the UI and render layer via subscribe (the latter reconciles the scene against list()).
|
|
61248
61331
|
*/
|
|
61249
61332
|
class AnnotationStore {
|
|
61250
61333
|
constructor() {
|
|
@@ -61287,7 +61370,7 @@ void main() {
|
|
|
61287
61370
|
}
|
|
61288
61371
|
this.notify();
|
|
61289
61372
|
}
|
|
61290
|
-
/**
|
|
61373
|
+
/** Clear all, returning the removed items (so the render layer can dispose three objects). */
|
|
61291
61374
|
clear() {
|
|
61292
61375
|
const old = this.items;
|
|
61293
61376
|
this.items = [];
|
|
@@ -61348,13 +61431,14 @@ void main() {
|
|
|
61348
61431
|
const LINE_W = 3;
|
|
61349
61432
|
const LINE_W_SEL = 6;
|
|
61350
61433
|
/**
|
|
61351
|
-
*
|
|
61352
|
-
* Enter
|
|
61353
|
-
*
|
|
61434
|
+
* Main surface-annotation controller (Phase 4): four modes navigate / freehand / geodesic / point,
|
|
61435
|
+
* Enter closes the loop, and data is managed by AnnotationStore (multiple color-labeled strips,
|
|
61436
|
+
* undo/delete/clear, export).
|
|
61437
|
+
* The render layer reconciles the scene against store.list(); undo/delete automatically add/remove three objects.
|
|
61354
61438
|
*/
|
|
61355
61439
|
class SurfaceAnnotator {
|
|
61356
61440
|
constructor(opts) {
|
|
61357
|
-
var _a, _b;
|
|
61441
|
+
var _a, _b, _c;
|
|
61358
61442
|
this.mode = "navigate";
|
|
61359
61443
|
this.spaceHeld = false;
|
|
61360
61444
|
this.pointerDown = false;
|
|
@@ -61362,12 +61446,12 @@ void main() {
|
|
|
61362
61446
|
this.managed = new Set();
|
|
61363
61447
|
this.seq = 0;
|
|
61364
61448
|
this.selectedId = null;
|
|
61365
|
-
this.activeGeoMarkers = []; //
|
|
61449
|
+
this.activeGeoMarkers = []; // visible anchors of the in-progress geodesic
|
|
61366
61450
|
this.geoRay = new Raycaster();
|
|
61367
61451
|
this.geoNdc = new Vector2();
|
|
61368
61452
|
this.hoveredGeoMarker = -1;
|
|
61369
61453
|
this._projV = new Vector3();
|
|
61370
|
-
/**
|
|
61454
|
+
/** Update the pixel resolution of all fat lines on window resize, otherwise line width distorts. */
|
|
61371
61455
|
this.onResize = () => {
|
|
61372
61456
|
const w = this.o.container.clientWidth;
|
|
61373
61457
|
const h = this.o.container.clientHeight;
|
|
@@ -61386,7 +61470,7 @@ void main() {
|
|
|
61386
61470
|
if (this.spaceHeld)
|
|
61387
61471
|
return;
|
|
61388
61472
|
if (e.button !== 0)
|
|
61389
|
-
return; //
|
|
61473
|
+
return; // left button only
|
|
61390
61474
|
if (!this.insideContainer(e))
|
|
61391
61475
|
return;
|
|
61392
61476
|
this.pointerDown = true;
|
|
@@ -61421,7 +61505,7 @@ void main() {
|
|
|
61421
61505
|
return;
|
|
61422
61506
|
}
|
|
61423
61507
|
if (this.mode === "geodesic") {
|
|
61424
|
-
//
|
|
61508
|
+
// First check whether an existing anchor was clicked → cancel that point (any point can be canceled).
|
|
61425
61509
|
const pick = this.pickGeoMarker(e);
|
|
61426
61510
|
if (pick >= 0 && this.activeGeo) {
|
|
61427
61511
|
this.activeGeo.removeAnchorAt(pick);
|
|
@@ -61433,7 +61517,7 @@ void main() {
|
|
|
61433
61517
|
}
|
|
61434
61518
|
return;
|
|
61435
61519
|
}
|
|
61436
|
-
//
|
|
61520
|
+
// Otherwise drop a new anchor on the surface.
|
|
61437
61521
|
const h = this.hit(e);
|
|
61438
61522
|
if (!h)
|
|
61439
61523
|
return;
|
|
@@ -61451,7 +61535,7 @@ void main() {
|
|
|
61451
61535
|
this.setGeoHover(-1);
|
|
61452
61536
|
return;
|
|
61453
61537
|
}
|
|
61454
|
-
//
|
|
61538
|
+
// In geodesic mode, show the "✕" hint when hovering an anchor sphere (that point can be clicked to cancel).
|
|
61455
61539
|
if (this.mode === "geodesic" && !this.pointerDown) {
|
|
61456
61540
|
this.setGeoHover(this.insideContainer(e) ? this.pickGeoMarker(e) : -1);
|
|
61457
61541
|
}
|
|
@@ -61516,7 +61600,7 @@ void main() {
|
|
|
61516
61600
|
return;
|
|
61517
61601
|
}
|
|
61518
61602
|
if (e.key === "Enter") {
|
|
61519
|
-
//
|
|
61603
|
+
// Geodesic in progress → finish and close it; otherwise close the most recent freehand line.
|
|
61520
61604
|
if (this.activeGeo)
|
|
61521
61605
|
this.finishGeodesic();
|
|
61522
61606
|
else
|
|
@@ -61541,24 +61625,25 @@ void main() {
|
|
|
61541
61625
|
this.o = opts;
|
|
61542
61626
|
const meshGeo = opts.mesh.geometry;
|
|
61543
61627
|
if (!meshGeo.getAttribute("normal"))
|
|
61544
|
-
meshGeo.computeVertexNormals(); //
|
|
61628
|
+
meshGeo.computeVertexNormals(); // rendering needs normals
|
|
61545
61629
|
meshGeo.computeBoundingBox();
|
|
61546
61630
|
const diag = (_a = opts.bboxDiagonal) !== null && _a !== void 0 ? _a : meshGeo.boundingBox.getSize(new Vector3()).length();
|
|
61547
61631
|
this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.0032;
|
|
61548
|
-
this.epsilon = diag * 0.002;
|
|
61632
|
+
this.epsilon = (_c = opts.epsilon) !== null && _c !== void 0 ? _c : diag * 0.002;
|
|
61549
61633
|
this.minGap = diag * 0.004;
|
|
61550
61634
|
this.maxJump = diag * 0.05;
|
|
61551
|
-
//
|
|
61552
|
-
//
|
|
61553
|
-
//
|
|
61635
|
+
// Geodesic connectivity: weld a separate "position-only" geometry for MeshGraph to use —
|
|
61636
|
+
// leaving the rendered mesh untouched (keeping its normals/UV/texture). Position-only ensures
|
|
61637
|
+
// vertices at the same surface position get merged by mergeVertices (otherwise per-face
|
|
61638
|
+
// normals/UV would block the merge, breaking the graph → the closed loop cuts through the model).
|
|
61554
61639
|
const posOnly = new BufferGeometry();
|
|
61555
61640
|
posOnly.setAttribute("position", meshGeo.getAttribute("position").clone());
|
|
61556
61641
|
const graphGeo = mergeVertices(posOnly);
|
|
61557
61642
|
graphGeo.computeVertexNormals();
|
|
61558
61643
|
this.graph = new MeshGraph(graphGeo);
|
|
61559
61644
|
this.store.subscribe(() => this.reconcile());
|
|
61560
|
-
//
|
|
61561
|
-
//
|
|
61645
|
+
// Listen on window in the capture phase (capture=true): receive every pointer event before
|
|
61646
|
+
// copper's TrackballControls, so its setPointerCapture / event routing doesn't drop moves mid-drag.
|
|
61562
61647
|
window.addEventListener("pointerdown", this.onPointerDown, true);
|
|
61563
61648
|
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
61564
61649
|
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
@@ -61579,13 +61664,13 @@ void main() {
|
|
|
61579
61664
|
var _a;
|
|
61580
61665
|
return (_a = this.o.pointColor) !== null && _a !== void 0 ? _a : "#ffd166";
|
|
61581
61666
|
}
|
|
61582
|
-
// ----
|
|
61667
|
+
// ---- Public API (called from Vue) ----
|
|
61583
61668
|
getMode() {
|
|
61584
61669
|
return this.mode;
|
|
61585
61670
|
}
|
|
61586
61671
|
setMode(m) {
|
|
61587
61672
|
var _a, _b;
|
|
61588
|
-
//
|
|
61673
|
+
// When leaving geodesic mode, discard the in-progress geodesic not yet committed with Enter (clear leftover anchors and line).
|
|
61589
61674
|
if (m !== "geodesic" && this.activeGeo)
|
|
61590
61675
|
this.clearActiveGeo();
|
|
61591
61676
|
this.mode = m;
|
|
@@ -61595,13 +61680,13 @@ void main() {
|
|
|
61595
61680
|
getStore() {
|
|
61596
61681
|
return this.store;
|
|
61597
61682
|
}
|
|
61598
|
-
/**
|
|
61683
|
+
/** Snapshot of the current annotation list. */
|
|
61599
61684
|
getAnnotations() {
|
|
61600
61685
|
return this.store.list();
|
|
61601
61686
|
}
|
|
61602
61687
|
undo() {
|
|
61603
|
-
//
|
|
61604
|
-
//
|
|
61688
|
+
// Geodesic in progress → undo the most recent anchor edit (both adding and canceling a point can be rolled back);
|
|
61689
|
+
// otherwise undo the most recently committed annotation.
|
|
61605
61690
|
if (this.activeGeo && this.activeGeo.canUndo()) {
|
|
61606
61691
|
this.activeGeo.undoEdit();
|
|
61607
61692
|
if (this.activeGeo.anchorCount === 0)
|
|
@@ -61635,7 +61720,7 @@ void main() {
|
|
|
61635
61720
|
this.selectedId = id;
|
|
61636
61721
|
this.applySelection();
|
|
61637
61722
|
}
|
|
61638
|
-
/**
|
|
61723
|
+
/** Redraw the corresponding three object after a color change. */
|
|
61639
61724
|
refreshAnnotation(id) {
|
|
61640
61725
|
const a = this.store.get(id);
|
|
61641
61726
|
if (!a || !a.object3D)
|
|
@@ -61651,12 +61736,12 @@ void main() {
|
|
|
61651
61736
|
exportJSON(modelName, opts) {
|
|
61652
61737
|
return this.store.toJSON(modelName, this.o.mesh, opts);
|
|
61653
61738
|
}
|
|
61654
|
-
// ----
|
|
61739
|
+
// ---- Internal ----
|
|
61655
61740
|
applyCameraGating() {
|
|
61656
|
-
//
|
|
61741
|
+
// Holding Space for temporary rotation takes priority; otherwise enable the camera only in navigate mode.
|
|
61657
61742
|
this.o.controls.enabled = this.spaceHeld || this.mode === "navigate";
|
|
61658
61743
|
}
|
|
61659
|
-
/**
|
|
61744
|
+
/** Reconcile the scene against store.list(): add missing objects, remove deleted ones (no dispose, kept for undo restore). */
|
|
61660
61745
|
reconcile() {
|
|
61661
61746
|
var _a, _b;
|
|
61662
61747
|
const present = new Set();
|
|
@@ -61706,29 +61791,53 @@ void main() {
|
|
|
61706
61791
|
return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
|
|
61707
61792
|
}
|
|
61708
61793
|
/**
|
|
61709
|
-
*
|
|
61710
|
-
*
|
|
61711
|
-
*
|
|
61712
|
-
*
|
|
61794
|
+
* Whether the event target is the WebGL canvas inside the container.
|
|
61795
|
+
* Only accept the canvas: panels (GUIDE / control panel / the ✕ buttons in the annotation list)
|
|
61796
|
+
* are all children of the container, so checking contains alone would treat clicks on the panels
|
|
61797
|
+
* as drawing on the model — which "leaks through" the delete buttons and makes ✕ hard to click.
|
|
61798
|
+
* Responding only to pointer events on the canvas fully isolates the UI from the drawing surface.
|
|
61713
61799
|
*/
|
|
61714
61800
|
insideContainer(e) {
|
|
61715
61801
|
const t = e.target;
|
|
61716
61802
|
return !!t && t.tagName === "CANVAS" && this.o.container.contains(t);
|
|
61717
61803
|
}
|
|
61718
|
-
/**
|
|
61804
|
+
/**
|
|
61805
|
+
* Pick an in-progress anchor: return the index of the nearest anchor (screen pixel distance <
|
|
61806
|
+
* tolerance), or -1 on a miss.
|
|
61807
|
+
*
|
|
61808
|
+
* Uses "screen-space pixel distance" instead of ray/sphere intersection: the anchor spheres are
|
|
61809
|
+
* tiny, so strict ray/sphere hits require pixel-perfect aiming and most clicks miss — and a miss
|
|
61810
|
+
* falls through to the "add anchor" branch, so trying to delete a point ends up adding one. That
|
|
61811
|
+
* was the root cause of the old "deleting a point takes several clicks, must hit the sphere dead
|
|
61812
|
+
* center" problem. Projecting each anchor to the screen and taking the nearest within tolerance
|
|
61813
|
+
* makes deletion stable and reliable. The tolerance is enlarged relative to the sphere's screen
|
|
61814
|
+
* radius to make it easy to hit.
|
|
61815
|
+
*/
|
|
61719
61816
|
pickGeoMarker(e) {
|
|
61720
61817
|
if (!this.activeGeoMarkers.length)
|
|
61721
61818
|
return -1;
|
|
61722
61819
|
const rect = this.o.container.getBoundingClientRect();
|
|
61723
|
-
|
|
61724
|
-
|
|
61725
|
-
|
|
61726
|
-
|
|
61727
|
-
|
|
61728
|
-
|
|
61729
|
-
|
|
61820
|
+
const px = e.clientX - rect.left;
|
|
61821
|
+
const py = e.clientY - rect.top;
|
|
61822
|
+
const TOL = 16; // hit tolerance (pixels), larger than the sphere itself for forgiving clicks
|
|
61823
|
+
let best = -1;
|
|
61824
|
+
let bestDist = TOL;
|
|
61825
|
+
const v = this._projV;
|
|
61826
|
+
for (let i = 0; i < this.activeGeoMarkers.length; i++) {
|
|
61827
|
+
v.copy(this.activeGeoMarkers[i].position).project(this.o.camera);
|
|
61828
|
+
if (v.z > 1)
|
|
61829
|
+
continue; // projects behind the camera, skip
|
|
61830
|
+
const sx = (v.x * 0.5 + 0.5) * rect.width;
|
|
61831
|
+
const sy = (-v.y * 0.5 + 0.5) * rect.height;
|
|
61832
|
+
const d = Math.hypot(sx - px, sy - py);
|
|
61833
|
+
if (d < bestDist) {
|
|
61834
|
+
bestDist = d;
|
|
61835
|
+
best = i;
|
|
61836
|
+
}
|
|
61837
|
+
}
|
|
61838
|
+
return best;
|
|
61730
61839
|
}
|
|
61731
|
-
/**
|
|
61840
|
+
/** Set the currently hovered anchor (-1 = none): update highlight scale, cursor, and the "✕ (cancel)" floating badge. */
|
|
61732
61841
|
setGeoHover(idx) {
|
|
61733
61842
|
if (idx === this.hoveredGeoMarker) {
|
|
61734
61843
|
if (idx >= 0)
|
|
@@ -61752,7 +61861,7 @@ void main() {
|
|
|
61752
61861
|
this.o.container.style.cursor = "";
|
|
61753
61862
|
}
|
|
61754
61863
|
}
|
|
61755
|
-
/**
|
|
61864
|
+
/** Lazily create the floating ✕ badge (appended inside the container, pointer-events:none so it doesn't block clicks). */
|
|
61756
61865
|
ensureGeoBadge() {
|
|
61757
61866
|
if (this.geoHoverBadge)
|
|
61758
61867
|
return this.geoHoverBadge;
|
|
@@ -61780,7 +61889,7 @@ void main() {
|
|
|
61780
61889
|
this.geoHoverBadge = b;
|
|
61781
61890
|
return b;
|
|
61782
61891
|
}
|
|
61783
|
-
/**
|
|
61892
|
+
/** Position the ✕ badge at the anchor's screen projection (dead center, i.e. where the cursor hovers, to avoid ambiguity). */
|
|
61784
61893
|
positionGeoBadge(marker) {
|
|
61785
61894
|
if (!marker)
|
|
61786
61895
|
return;
|
|
@@ -61792,7 +61901,7 @@ void main() {
|
|
|
61792
61901
|
b.style.left = `${x}px`;
|
|
61793
61902
|
b.style.top = `${y}px`;
|
|
61794
61903
|
}
|
|
61795
|
-
/**
|
|
61904
|
+
/** Rebuild the visible anchor spheres from the current anchors (slightly larger than placed points, so they're easy to see and click to cancel). */
|
|
61796
61905
|
rebuildGeoMarkers() {
|
|
61797
61906
|
this.setGeoHover(-1);
|
|
61798
61907
|
for (const m of this.activeGeoMarkers) {
|
|
@@ -61808,7 +61917,7 @@ void main() {
|
|
|
61808
61917
|
this.activeGeoMarkers.push(marker);
|
|
61809
61918
|
}
|
|
61810
61919
|
}
|
|
61811
|
-
/**
|
|
61920
|
+
/** Redraw the in-progress geodesic from the current anchors (not closed); remove the line when fewer than two points. */
|
|
61812
61921
|
redrawGeoLine() {
|
|
61813
61922
|
if (!this.activeGeo)
|
|
61814
61923
|
return;
|
|
@@ -61829,7 +61938,7 @@ void main() {
|
|
|
61829
61938
|
updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
|
|
61830
61939
|
}
|
|
61831
61940
|
}
|
|
61832
|
-
/**
|
|
61941
|
+
/** Discard the in-progress geodesic: remove and dispose the line and all anchors. */
|
|
61833
61942
|
clearActiveGeo() {
|
|
61834
61943
|
this.setGeoHover(-1);
|
|
61835
61944
|
if (this.activeGeoLine) {
|
|
@@ -61844,7 +61953,7 @@ void main() {
|
|
|
61844
61953
|
this.activeGeoMarkers = [];
|
|
61845
61954
|
this.activeGeo = undefined;
|
|
61846
61955
|
}
|
|
61847
|
-
/**
|
|
61956
|
+
/** Remove all anchor spheres of the in-progress geodesic (called after committing: only the line remains). */
|
|
61848
61957
|
removeGeoMarkers() {
|
|
61849
61958
|
this.setGeoHover(-1);
|
|
61850
61959
|
for (const m of this.activeGeoMarkers) {
|
|
@@ -61857,7 +61966,7 @@ void main() {
|
|
|
61857
61966
|
const last = this.lastFreehand;
|
|
61858
61967
|
if (!last || last.closed || last.vertices.length < 3 || !last.object3D)
|
|
61859
61968
|
return;
|
|
61860
|
-
//
|
|
61969
|
+
// 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.
|
|
61861
61970
|
const tail = last.vertices[last.vertices.length - 1];
|
|
61862
61971
|
const head = last.vertices[0];
|
|
61863
61972
|
const closing = this.surfacePathBetween(tail, head);
|
|
@@ -61865,14 +61974,14 @@ void main() {
|
|
|
61865
61974
|
last.closed = true;
|
|
61866
61975
|
updateContourLine(last.object3D, last.vertices, true, this.epsilon, this.o.mesh);
|
|
61867
61976
|
}
|
|
61868
|
-
/**
|
|
61977
|
+
/** 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. */
|
|
61869
61978
|
surfacePathBetween(a, b) {
|
|
61870
61979
|
const va = this.graph.nearestVertex(new Vector3(a.x, a.y, a.z));
|
|
61871
61980
|
const vb = this.graph.nearestVertex(new Vector3(b.x, b.y, b.z));
|
|
61872
61981
|
const path = this.graph.shortestPath(va, vb);
|
|
61873
61982
|
return path.slice(1).map((i) => this.graph.vertexLocal(i));
|
|
61874
61983
|
}
|
|
61875
|
-
/**
|
|
61984
|
+
/** Finish the current geodesic: close it into a ring and commit it as one contour. */
|
|
61876
61985
|
finishGeodesic() {
|
|
61877
61986
|
if (!this.activeGeo || !this.activeGeoLine)
|
|
61878
61987
|
return;
|
|
@@ -61896,7 +62005,7 @@ void main() {
|
|
|
61896
62005
|
this.o.scene.remove(this.activeGeoLine);
|
|
61897
62006
|
this.disposeObject(this.activeGeoLine);
|
|
61898
62007
|
}
|
|
61899
|
-
//
|
|
62008
|
+
// After committing, clear the visible anchors and leave only the surface-hugging line.
|
|
61900
62009
|
this.removeGeoMarkers();
|
|
61901
62010
|
this.activeGeo = undefined;
|
|
61902
62011
|
this.activeGeoLine = undefined;
|
|
@@ -62011,9 +62120,9 @@ void main() {
|
|
|
62011
62120
|
copperVtkLoader(url, this.scene, this.content);
|
|
62012
62121
|
}
|
|
62013
62122
|
/**
|
|
62014
|
-
*
|
|
62015
|
-
* target
|
|
62016
|
-
*
|
|
62123
|
+
* Create an annotator on the surface of a given model (draw contours / place points / export coordinates).
|
|
62124
|
+
* target can be a single Mesh, or a Group/Object3D (the mesh with the most vertices is chosen automatically).
|
|
62125
|
+
* Reuses this scene's camera / container / controls; non-indexed geometry is welded and indexed automatically.
|
|
62017
62126
|
*/
|
|
62018
62127
|
createSurfaceAnnotator(target, opts) {
|
|
62019
62128
|
const mesh = this.pickAnnotatableMesh(target);
|
|
@@ -62041,7 +62150,7 @@ void main() {
|
|
|
62041
62150
|
});
|
|
62042
62151
|
return best;
|
|
62043
62152
|
}
|
|
62044
|
-
/**
|
|
62153
|
+
/** Dispose all annotators created on this scene (removes event listeners and annotation objects). */
|
|
62045
62154
|
disposeSurfaceAnnotators() {
|
|
62046
62155
|
this.surfaceAnnotators.forEach((a) => a.dispose());
|
|
62047
62156
|
this.surfaceAnnotators = [];
|
|
@@ -79015,14 +79124,29 @@ void main() {
|
|
|
79015
79124
|
this.committedVol = null;
|
|
79016
79125
|
this.promptTool = "point";
|
|
79017
79126
|
this.polarity = 1; // 1 = foreground (positive), 0 = background
|
|
79018
|
-
/**
|
|
79019
|
-
|
|
79127
|
+
/** The label value (1-255) the AI currently paints into = the active Segmentation.
|
|
79128
|
+
* Each Segmentation is a label value in the single-channel scratch volume; its
|
|
79129
|
+
* colour lives in the volume's colorMap (set via setSegmentColor). */
|
|
79130
|
+
this.activeLabel = 1;
|
|
79020
79131
|
/** Scribble brush radius (px), user-adjustable via slider. */
|
|
79021
79132
|
this.scribbleSize = 5;
|
|
79022
79133
|
/** Accumulated prompts for the CURRENT slice; reset on slice/axis change. */
|
|
79023
79134
|
this.points = [];
|
|
79024
79135
|
this.scribble = [];
|
|
79136
|
+
/** Lasso contour sent to the backend (densified along the curve at finish). */
|
|
79137
|
+
this.lasso = [];
|
|
79025
79138
|
this.activeSlice = -1;
|
|
79139
|
+
// ── Lasso v2 editing state (discrete clicked vertices → smooth closed curve) ──
|
|
79140
|
+
/** Placed lasso vertices (voxel + screen + polarity). Empty = not editing. */
|
|
79141
|
+
this.lassoVerts = [];
|
|
79142
|
+
/** Undo / redo snapshot stacks of the vertex list (for Ctrl+Z / Ctrl+Y). */
|
|
79143
|
+
this.lassoUndo = [];
|
|
79144
|
+
this.lassoRedo = [];
|
|
79145
|
+
/** Index of the vertex currently hovered (shows a red ✕; click removes it). −1 = none. */
|
|
79146
|
+
this.lassoHoverIdx = -1;
|
|
79147
|
+
/** Double-click detection (a dbl-click finishes the lasso). */
|
|
79148
|
+
this.lastLassoDownTime = 0;
|
|
79149
|
+
this.lastLassoDownPos = { x: 0, y: 0 };
|
|
79026
79150
|
// Drag state (box / scribble) — voxel coords for the prompt
|
|
79027
79151
|
this.dragging = false;
|
|
79028
79152
|
this.dragStart = null;
|
|
@@ -79033,8 +79157,14 @@ void main() {
|
|
|
79033
79157
|
// Live cursor position (screen px) for the scribble brush-size preview ring —
|
|
79034
79158
|
// updated on every hover move so the ring follows the mouse like the paint brush.
|
|
79035
79159
|
this.hoverScreen = null;
|
|
79160
|
+
/** Hide the point seed markers once a prediction has painted (only the mask
|
|
79161
|
+
* should remain). A new point click re-shows them until its mask returns. */
|
|
79162
|
+
this.hidePointMarkers = false;
|
|
79036
79163
|
/** Fired when a prompt gesture completes — app calls backend, then applyMask(). */
|
|
79037
79164
|
this.onPrompt = null;
|
|
79165
|
+
/** Fired whenever the lasso vertex set changes (add/delete/undo/redo/finish/cancel),
|
|
79166
|
+
* so the panel can reactively show the Finish button + vertex count. */
|
|
79167
|
+
this.onLassoChange = null;
|
|
79038
79168
|
this.host = host;
|
|
79039
79169
|
}
|
|
79040
79170
|
// ── Configuration (driven from the panel via NrrdTools) ────────────────────
|
|
@@ -79052,16 +79182,51 @@ void main() {
|
|
|
79052
79182
|
setScribbleSize(size) {
|
|
79053
79183
|
this.scribbleSize = Math.max(1, Math.min(40, Math.round(size)));
|
|
79054
79184
|
}
|
|
79055
|
-
|
|
79056
|
-
|
|
79057
|
-
|
|
79058
|
-
|
|
79059
|
-
|
|
79060
|
-
if (c !== this.
|
|
79185
|
+
/** Select the active Segmentation by its label value (1-255). Switching starts a
|
|
79186
|
+
* fresh prompt set — otherwise accumulated prompts would be re-predicted and
|
|
79187
|
+
* repainted into the new label, recolouring the previous segmentation's region. */
|
|
79188
|
+
setActiveSegment(label) {
|
|
79189
|
+
const c = Math.max(1, Math.min(255, Math.round(label)));
|
|
79190
|
+
if (c !== this.activeLabel)
|
|
79061
79191
|
this.resetPrompts();
|
|
79062
|
-
this.
|
|
79192
|
+
this.activeLabel = c;
|
|
79193
|
+
}
|
|
79194
|
+
getActiveLabel() { return this.activeLabel; }
|
|
79195
|
+
/** Set a Segmentation's colour (its label's entry in the scratch volume colorMap).
|
|
79196
|
+
* The 2D overlay repaints next frame (copper3d's render loop re-reads the map). */
|
|
79197
|
+
setSegmentColor(label, color) {
|
|
79198
|
+
var _a;
|
|
79199
|
+
(_a = this.scratchVol) === null || _a === void 0 ? void 0 : _a.setChannelColor(Math.round(label), color);
|
|
79200
|
+
}
|
|
79201
|
+
/** "New segmentation": freeze the current regions (so they persist) and switch to
|
|
79202
|
+
* a fresh label, exactly like the old "New region" but targeting a new label. */
|
|
79203
|
+
newSegment(label) {
|
|
79204
|
+
this.commitRegion();
|
|
79205
|
+
this.setActiveSegment(label);
|
|
79206
|
+
}
|
|
79207
|
+
/** Delete a Segmentation: erase every voxel painted with its label from BOTH the
|
|
79208
|
+
* live scratch and the frozen (committed) volume, so the region disappears from
|
|
79209
|
+
* the view (otherwise orphaned label voxels would render with the wrong colour).
|
|
79210
|
+
* Goes via setRawData so the volume's version counter bumps (overlay repaints). */
|
|
79211
|
+
clearSegment(label) {
|
|
79212
|
+
const lbl = Math.round(label);
|
|
79213
|
+
const zeroOut = (v) => {
|
|
79214
|
+
if (!v)
|
|
79215
|
+
return;
|
|
79216
|
+
const raw = v.getRawData().slice();
|
|
79217
|
+
let changed = false;
|
|
79218
|
+
for (let i = 0; i < raw.length; i++) {
|
|
79219
|
+
if (raw[i] === lbl) {
|
|
79220
|
+
raw[i] = 0;
|
|
79221
|
+
changed = true;
|
|
79222
|
+
}
|
|
79223
|
+
}
|
|
79224
|
+
if (changed)
|
|
79225
|
+
v.setRawData(raw);
|
|
79226
|
+
};
|
|
79227
|
+
zeroOut(this.scratchVol);
|
|
79228
|
+
zeroOut(this.committedVol);
|
|
79063
79229
|
}
|
|
79064
|
-
getChannel() { return this.channel; }
|
|
79065
79230
|
getScratchVolume() { return this.scratchVol; }
|
|
79066
79231
|
/** Serialize the scratch volume as per-z-slice RLE (only NON-empty slices),
|
|
79067
79232
|
* for persisting to ai_generated_nii_LPS on the backend. Binary (any channel →
|
|
@@ -79103,6 +79268,50 @@ void main() {
|
|
|
79103
79268
|
}
|
|
79104
79269
|
return { axis: "z", width, height, slices: out };
|
|
79105
79270
|
}
|
|
79271
|
+
/** Per-SEGMENTATION serialization for the 3D build: one binary RLE per non-empty
|
|
79272
|
+
* z-slice PER label (no binarize-merge), so the backend writes a multi-label
|
|
79273
|
+
* NIfTI and colours the GLB per segmentation. Includes both live and frozen
|
|
79274
|
+
* regions (both live in scratchVol). */
|
|
79275
|
+
getScratchSegments() {
|
|
79276
|
+
if (!this.scratchVol)
|
|
79277
|
+
return null;
|
|
79278
|
+
const { depth } = this.scratchVol.getDimensions();
|
|
79279
|
+
const byLabel = new Map();
|
|
79280
|
+
let width = 0;
|
|
79281
|
+
let height = 0;
|
|
79282
|
+
for (let z = 0; z < depth; z++) {
|
|
79283
|
+
const { data, width: w, height: h } = this.scratchVol.getSliceUint8(z, "z");
|
|
79284
|
+
width = w;
|
|
79285
|
+
height = h;
|
|
79286
|
+
const labels = new Set();
|
|
79287
|
+
for (let i = 0; i < data.length; i++) {
|
|
79288
|
+
if (data[i])
|
|
79289
|
+
labels.add(data[i]);
|
|
79290
|
+
}
|
|
79291
|
+
for (const label of labels) {
|
|
79292
|
+
const runs = [];
|
|
79293
|
+
let value = 0; // always begin with a 0-run (matches backend rle_decode)
|
|
79294
|
+
let count = 0;
|
|
79295
|
+
for (let i = 0; i < data.length; i++) {
|
|
79296
|
+
const v = data[i] === label ? 1 : 0;
|
|
79297
|
+
if (v === value) {
|
|
79298
|
+
count++;
|
|
79299
|
+
}
|
|
79300
|
+
else {
|
|
79301
|
+
runs.push(count);
|
|
79302
|
+
value = v;
|
|
79303
|
+
count = 1;
|
|
79304
|
+
}
|
|
79305
|
+
}
|
|
79306
|
+
runs.push(count);
|
|
79307
|
+
if (!byLabel.has(label))
|
|
79308
|
+
byLabel.set(label, []);
|
|
79309
|
+
byLabel.get(label).push({ sliceIndex: z, rle: runs });
|
|
79310
|
+
}
|
|
79311
|
+
}
|
|
79312
|
+
const segments = [...byLabel.entries()].map(([label, slices]) => ({ label, slices }));
|
|
79313
|
+
return { axis: "z", width, height, segments };
|
|
79314
|
+
}
|
|
79106
79315
|
// ── Sandbox lifecycle ──────────────────────────────────────────────────────
|
|
79107
79316
|
/** Create (or reuse) the scratch volume sized to the layer grid and register
|
|
79108
79317
|
* it under maskData.volumes['aiScratch']. Snapshots the empty buffer. */
|
|
@@ -79123,14 +79332,10 @@ void main() {
|
|
|
79123
79332
|
this.scratchVol.clear();
|
|
79124
79333
|
}
|
|
79125
79334
|
volumes[AI_SCRATCH_LAYER] = this.scratchVol;
|
|
79126
|
-
//
|
|
79127
|
-
//
|
|
79128
|
-
//
|
|
79129
|
-
|
|
79130
|
-
const c = AI_MASK_CHANNEL_COLORS[ch];
|
|
79131
|
-
if (c)
|
|
79132
|
-
this.scratchVol.setChannelColor(ch, { r: c.r, g: c.g, b: c.b, a: c.a });
|
|
79133
|
-
}
|
|
79335
|
+
// Segmentation colours are now driven per-label from the app (store → useAiAssist
|
|
79336
|
+
// → setSegmentColor) right after enter, so the scratch can carry an arbitrary
|
|
79337
|
+
// number of independently-coloured segmentations. No fixed palette is applied
|
|
79338
|
+
// here. Scoped to THIS volume only — global/clinician palettes are untouched.
|
|
79134
79339
|
this.snapshotData = this.scratchVol.getRawData().slice();
|
|
79135
79340
|
this.committedVol = null; // nothing frozen at session start
|
|
79136
79341
|
this.resetPrompts();
|
|
@@ -79176,10 +79381,26 @@ void main() {
|
|
|
79176
79381
|
resetPrompts() {
|
|
79177
79382
|
this.points = [];
|
|
79178
79383
|
this.scribble = [];
|
|
79384
|
+
this.lasso = [];
|
|
79179
79385
|
this.box = undefined;
|
|
79180
79386
|
this.dragScreenStart = null;
|
|
79181
79387
|
this.dragScreen = null;
|
|
79182
79388
|
this.screenScribble = [];
|
|
79389
|
+
this.clearLassoEditing();
|
|
79390
|
+
}
|
|
79391
|
+
/** Drop the in-progress lasso vertices + undo/redo history (does not touch a
|
|
79392
|
+
* prediction already painted from a finished lasso). */
|
|
79393
|
+
clearLassoEditing() {
|
|
79394
|
+
this.lassoVerts = [];
|
|
79395
|
+
this.lassoUndo = [];
|
|
79396
|
+
this.lassoRedo = [];
|
|
79397
|
+
this.lassoHoverIdx = -1;
|
|
79398
|
+
this.notifyLasso();
|
|
79399
|
+
}
|
|
79400
|
+
/** Tell the app layer the lasso vertex set changed (drives the Finish button). */
|
|
79401
|
+
notifyLasso() {
|
|
79402
|
+
var _a;
|
|
79403
|
+
(_a = this.onLassoChange) === null || _a === void 0 ? void 0 : _a.call(this, this.lassoVerts.length, this.lassoVerts.length > 0);
|
|
79183
79404
|
}
|
|
79184
79405
|
// ── Coordinate mapping (all three views) ───────────────────────────────────
|
|
79185
79406
|
//
|
|
@@ -79240,14 +79461,53 @@ void main() {
|
|
|
79240
79461
|
return null;
|
|
79241
79462
|
return { vx, vy, w: sd.w, h: sd.h };
|
|
79242
79463
|
}
|
|
79464
|
+
/** Voxel-slice (vx,vy) → screen offset (px). Exact inverse of toVoxel, so
|
|
79465
|
+
* markers / lasso curves drawn from voxel coords track pan AND zoom each frame
|
|
79466
|
+
* (the freehand previews store screen px directly; persistent overlays don't). */
|
|
79467
|
+
voxelToScreen(vx, vy) {
|
|
79468
|
+
const sd = this.sliceDims();
|
|
79469
|
+
if (!sd)
|
|
79470
|
+
return null;
|
|
79471
|
+
const nrrd = this.ctx.nrrd_states;
|
|
79472
|
+
const img = nrrd.image;
|
|
79473
|
+
let hMM, vMM, flipV;
|
|
79474
|
+
switch (this.ctx.protectedData.axis) {
|
|
79475
|
+
case "z":
|
|
79476
|
+
hMM = img.nrrd_x_mm;
|
|
79477
|
+
vMM = img.nrrd_y_mm;
|
|
79478
|
+
flipV = false;
|
|
79479
|
+
break;
|
|
79480
|
+
case "y":
|
|
79481
|
+
hMM = img.nrrd_x_mm;
|
|
79482
|
+
vMM = img.nrrd_z_mm;
|
|
79483
|
+
flipV = true;
|
|
79484
|
+
break;
|
|
79485
|
+
case "x":
|
|
79486
|
+
hMM = img.nrrd_z_mm;
|
|
79487
|
+
vMM = img.nrrd_y_mm;
|
|
79488
|
+
flipV = false;
|
|
79489
|
+
break;
|
|
79490
|
+
default: return null;
|
|
79491
|
+
}
|
|
79492
|
+
const vyRaw = flipV ? sd.h - 1 - vy : vy;
|
|
79493
|
+
const x = (vx * hMM / sd.w) * nrrd.view.sizeFactor;
|
|
79494
|
+
const y = (vyRaw * vMM / sd.h) * nrrd.view.sizeFactor;
|
|
79495
|
+
return { x, y };
|
|
79496
|
+
}
|
|
79243
79497
|
// ── Pointer handlers (left button; right stays pan in EventRouter) ──────────
|
|
79244
79498
|
onPointerDown(e) {
|
|
79245
79499
|
const hit = this.toVoxel(e);
|
|
79246
79500
|
if (!hit)
|
|
79247
79501
|
return;
|
|
79248
79502
|
this.syncSlice();
|
|
79503
|
+
// Lasso is click-to-place vertices (NOT a drag) — own handler.
|
|
79504
|
+
if (this.promptTool === "lasso") {
|
|
79505
|
+
this.onLassoPointerDown(e, hit.vx, hit.vy);
|
|
79506
|
+
return;
|
|
79507
|
+
}
|
|
79249
79508
|
if (this.promptTool === "point") {
|
|
79250
79509
|
this.points.push({ x: hit.vx, y: hit.vy, label: this.polarity });
|
|
79510
|
+
this.hidePointMarkers = false; // show the new seed until its mask returns
|
|
79251
79511
|
this.emit();
|
|
79252
79512
|
}
|
|
79253
79513
|
else {
|
|
@@ -79263,9 +79523,43 @@ void main() {
|
|
|
79263
79523
|
}
|
|
79264
79524
|
}
|
|
79265
79525
|
}
|
|
79526
|
+
/** Lasso click: double-click finishes; click on a vertex deletes it; otherwise
|
|
79527
|
+
* add a vertex. Nothing is sent to the backend until finishLasso(). */
|
|
79528
|
+
onLassoPointerDown(e, vx, vy) {
|
|
79529
|
+
const now = Date.now();
|
|
79530
|
+
const dxl = e.offsetX - this.lastLassoDownPos.x;
|
|
79531
|
+
const dyl = e.offsetY - this.lastLassoDownPos.y;
|
|
79532
|
+
const isDbl = now - this.lastLassoDownTime < 300 &&
|
|
79533
|
+
dxl * dxl + dyl * dyl <= 36; // ~6px
|
|
79534
|
+
this.lastLassoDownTime = now;
|
|
79535
|
+
this.lastLassoDownPos = { x: e.offsetX, y: e.offsetY };
|
|
79536
|
+
// Double-click → finish (the 1st click of the pair already placed the vertex).
|
|
79537
|
+
if (isDbl && this.lassoVerts.length >= 3) {
|
|
79538
|
+
this.finishLasso();
|
|
79539
|
+
return;
|
|
79540
|
+
}
|
|
79541
|
+
// Click on an existing vertex → delete it (recompute the curve).
|
|
79542
|
+
const idx = this.lassoVertAt(e.offsetX, e.offsetY);
|
|
79543
|
+
if (idx >= 0) {
|
|
79544
|
+
this.pushLassoUndo();
|
|
79545
|
+
this.lassoVerts.splice(idx, 1);
|
|
79546
|
+
this.lassoHoverIdx = -1;
|
|
79547
|
+
this.notifyLasso();
|
|
79548
|
+
return;
|
|
79549
|
+
}
|
|
79550
|
+
// Otherwise add a new vertex.
|
|
79551
|
+
this.pushLassoUndo();
|
|
79552
|
+
this.lassoVerts.push({ vx, vy, sx: e.offsetX, sy: e.offsetY, label: this.polarity });
|
|
79553
|
+
this.notifyLasso();
|
|
79554
|
+
}
|
|
79266
79555
|
onPointerMove(e) {
|
|
79267
|
-
// Track hover for the scribble preview ring even when
|
|
79556
|
+
// Track hover for the scribble preview ring / point crosshair even when idle.
|
|
79268
79557
|
this.hoverScreen = { x: e.offsetX, y: e.offsetY };
|
|
79558
|
+
// Lasso: update which vertex (if any) is hovered, for the red ✕ delete affordance.
|
|
79559
|
+
if (this.promptTool === "lasso") {
|
|
79560
|
+
this.lassoHoverIdx = this.lassoVertAt(e.offsetX, e.offsetY);
|
|
79561
|
+
return; // lasso is click-based; no drag accumulation
|
|
79562
|
+
}
|
|
79269
79563
|
if (!this.dragging)
|
|
79270
79564
|
return;
|
|
79271
79565
|
// Track screen pos first so the live preview follows even slightly out of bounds.
|
|
@@ -79318,12 +79612,116 @@ void main() {
|
|
|
79318
79612
|
box: this.box,
|
|
79319
79613
|
scribble: this.scribble.length ? [...this.scribble] : undefined,
|
|
79320
79614
|
scribbleRadius: this.scribbleSize,
|
|
79615
|
+
lasso: this.lasso.length ? [...this.lasso] : undefined,
|
|
79321
79616
|
});
|
|
79322
79617
|
}
|
|
79618
|
+
// ── Lasso v2 (discrete vertices → smooth closed curve) ──────────────────────
|
|
79619
|
+
/** Index of the lasso vertex under (sx,sy) within the hit radius, else −1.
|
|
79620
|
+
* Uses live voxel→screen so it tracks pan/zoom. Topmost (latest) wins. */
|
|
79621
|
+
lassoVertAt(sx, sy) {
|
|
79622
|
+
var _a;
|
|
79623
|
+
const r2 = AiAssistTool.LASSO_HIT_R * AiAssistTool.LASSO_HIT_R;
|
|
79624
|
+
for (let i = this.lassoVerts.length - 1; i >= 0; i--) {
|
|
79625
|
+
const v = this.lassoVerts[i];
|
|
79626
|
+
const s = (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy };
|
|
79627
|
+
const dx = sx - s.x, dy = sy - s.y;
|
|
79628
|
+
if (dx * dx + dy * dy <= r2)
|
|
79629
|
+
return i;
|
|
79630
|
+
}
|
|
79631
|
+
return -1;
|
|
79632
|
+
}
|
|
79633
|
+
/** Snapshot the vertex list for undo (clears redo). */
|
|
79634
|
+
pushLassoUndo() {
|
|
79635
|
+
this.lassoUndo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79636
|
+
this.lassoRedo = [];
|
|
79637
|
+
if (this.lassoUndo.length > 100)
|
|
79638
|
+
this.lassoUndo.shift();
|
|
79639
|
+
}
|
|
79640
|
+
/** Public: undo the last add/delete of a lasso vertex (Ctrl+Z while editing). */
|
|
79641
|
+
lassoUndoAction() {
|
|
79642
|
+
if (!this.lassoUndo.length)
|
|
79643
|
+
return;
|
|
79644
|
+
this.lassoRedo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79645
|
+
this.lassoVerts = this.lassoUndo.pop();
|
|
79646
|
+
this.lassoHoverIdx = -1;
|
|
79647
|
+
this.notifyLasso();
|
|
79648
|
+
}
|
|
79649
|
+
/** Public: redo (Ctrl+Y / Ctrl+Shift+Z while editing). */
|
|
79650
|
+
lassoRedoAction() {
|
|
79651
|
+
if (!this.lassoRedo.length)
|
|
79652
|
+
return;
|
|
79653
|
+
this.lassoUndo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79654
|
+
this.lassoVerts = this.lassoRedo.pop();
|
|
79655
|
+
this.lassoHoverIdx = -1;
|
|
79656
|
+
this.notifyLasso();
|
|
79657
|
+
}
|
|
79658
|
+
/** Public: abandon the in-progress lasso (Esc). */
|
|
79659
|
+
cancelLasso() { this.clearLassoEditing(); }
|
|
79660
|
+
/** Public: true while the user is placing lasso vertices (≥1 vertex). */
|
|
79661
|
+
isLassoEditing() { return this.lassoVerts.length > 0; }
|
|
79662
|
+
/** Public: number of placed lasso vertices (drives the Finish button label/enable). */
|
|
79663
|
+
getLassoVertCount() { return this.lassoVerts.length; }
|
|
79664
|
+
/** Vertices ordered by polar angle around their centroid, so the closed curve is
|
|
79665
|
+
* a SIMPLE (non-self-intersecting) loop regardless of click order — otherwise
|
|
79666
|
+
* connecting points in click order + closing easily produces a bow-tie crossing.
|
|
79667
|
+
* Trade-off: deep concavities collapse to their star-shaped hull, which is fine
|
|
79668
|
+
* for a "rough loop around a blob". */
|
|
79669
|
+
orderedLassoVerts() {
|
|
79670
|
+
const n = this.lassoVerts.length;
|
|
79671
|
+
if (n < 3)
|
|
79672
|
+
return this.lassoVerts.slice();
|
|
79673
|
+
let cx = 0, cy = 0;
|
|
79674
|
+
for (const v of this.lassoVerts) {
|
|
79675
|
+
cx += v.vx;
|
|
79676
|
+
cy += v.vy;
|
|
79677
|
+
}
|
|
79678
|
+
cx /= n;
|
|
79679
|
+
cy /= n;
|
|
79680
|
+
return this.lassoVerts.slice().sort((a, b) => Math.atan2(a.vy - cy, a.vx - cx) - Math.atan2(b.vy - cy, b.vx - cx));
|
|
79681
|
+
}
|
|
79682
|
+
/** Public: close the curve, densify it, and send it to the backend as the lasso
|
|
79683
|
+
* contour. Needs ≥3 vertices (a real area). Then clears the editing state. */
|
|
79684
|
+
finishLasso() {
|
|
79685
|
+
if (this.lassoVerts.length < 3)
|
|
79686
|
+
return;
|
|
79687
|
+
// Order by angle (no self-crossing), then sample the SAME closed curve the user
|
|
79688
|
+
// saw → contour points (so the mask matches the curve, not a straight polygon).
|
|
79689
|
+
const contour = this.sampleClosedSpline(this.orderedLassoVerts().map((v) => ({ x: v.vx, y: v.vy })), 16);
|
|
79690
|
+
const label = this.lassoVerts[0].label; // a lasso carries one polarity (set at start)
|
|
79691
|
+
this.lasso = contour.map((p) => ({ x: Math.round(p.x), y: Math.round(p.y), label }));
|
|
79692
|
+
this.clearLassoEditing();
|
|
79693
|
+
this.emit();
|
|
79694
|
+
}
|
|
79695
|
+
/** Sample a smooth CLOSED Catmull-Rom spline through `pts` → denser polyline.
|
|
79696
|
+
* `segs` samples per control-point span. <3 pts → returns a copy unchanged. */
|
|
79697
|
+
sampleClosedSpline(pts, segs) {
|
|
79698
|
+
const n = pts.length;
|
|
79699
|
+
if (n < 3)
|
|
79700
|
+
return pts.slice();
|
|
79701
|
+
const at = (i) => pts[((i % n) + n) % n];
|
|
79702
|
+
const out = [];
|
|
79703
|
+
for (let i = 0; i < n; i++) {
|
|
79704
|
+
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
|
|
79705
|
+
for (let s = 0; s < segs; s++) {
|
|
79706
|
+
const t = s / segs, t2 = t * t, t3 = t2 * t;
|
|
79707
|
+
out.push({
|
|
79708
|
+
x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t +
|
|
79709
|
+
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
|
|
79710
|
+
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
|
|
79711
|
+
y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t +
|
|
79712
|
+
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
|
|
79713
|
+
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
|
|
79714
|
+
});
|
|
79715
|
+
}
|
|
79716
|
+
}
|
|
79717
|
+
return out;
|
|
79718
|
+
}
|
|
79323
79719
|
// ── Apply backend result → scratch volume ──────────────────────────────────
|
|
79324
79720
|
applyMask(result) {
|
|
79325
79721
|
if (!this.scratchVol)
|
|
79326
79722
|
return;
|
|
79723
|
+
// A prediction landed → hide the point seed markers; only the mask should show.
|
|
79724
|
+
this.hidePointMarkers = true;
|
|
79327
79725
|
// 3D result (engine B): write each slice across the covered span.
|
|
79328
79726
|
if (result.slices && result.sliceRange) {
|
|
79329
79727
|
const [lo] = result.sliceRange;
|
|
@@ -79353,7 +79751,7 @@ void main() {
|
|
|
79353
79751
|
pos += run;
|
|
79354
79752
|
value ^= 1;
|
|
79355
79753
|
}
|
|
79356
|
-
const ch = this.
|
|
79754
|
+
const ch = this.activeLabel;
|
|
79357
79755
|
try {
|
|
79358
79756
|
const out = this.scratchVol.getSliceUint8(sliceIndex, axis).data; // copy
|
|
79359
79757
|
// Frozen same-channel pixels for this slice (if any region was committed).
|
|
@@ -79426,10 +79824,7 @@ void main() {
|
|
|
79426
79824
|
}
|
|
79427
79825
|
targetCtx.restore();
|
|
79428
79826
|
}
|
|
79429
|
-
// Scribble brush-size preview ring (when NOT dragging)
|
|
79430
|
-
// the cursor whose radius = scribbleSize, so the user sees the brush size and
|
|
79431
|
-
// how the slider changes it (mirrors the paint brush's preview ring). Drawn
|
|
79432
|
-
// every frame from the live hover position by copper3d's render loop.
|
|
79827
|
+
// Scribble brush-size preview ring (when NOT dragging).
|
|
79433
79828
|
if (this.promptTool === "scribble" && !this.dragging && this.hoverScreen) {
|
|
79434
79829
|
targetCtx.save();
|
|
79435
79830
|
const fg = this.polarity === 1;
|
|
@@ -79442,8 +79837,105 @@ void main() {
|
|
|
79442
79837
|
targetCtx.stroke();
|
|
79443
79838
|
targetCtx.restore();
|
|
79444
79839
|
}
|
|
79840
|
+
// Point markers + hover crosshair so the clinician sees exactly where each seed
|
|
79841
|
+
// lands (and where the next click will go) before/while the prediction returns.
|
|
79842
|
+
if (this.promptTool === "point") {
|
|
79843
|
+
if (!this.hidePointMarkers) {
|
|
79844
|
+
for (const p of this.points) {
|
|
79845
|
+
const s = this.voxelToScreen(p.x, p.y);
|
|
79846
|
+
if (s)
|
|
79847
|
+
this.drawSeedMarker(targetCtx, s.x, s.y, p.label === 1);
|
|
79848
|
+
}
|
|
79849
|
+
}
|
|
79850
|
+
if (!this.dragging && this.hoverScreen) {
|
|
79851
|
+
targetCtx.save();
|
|
79852
|
+
targetCtx.strokeStyle = this.polarity === 1
|
|
79853
|
+
? "rgba(120,255,180,0.55)" : "rgba(255,120,120,0.55)";
|
|
79854
|
+
targetCtx.lineWidth = 1;
|
|
79855
|
+
const { x, y } = this.hoverScreen;
|
|
79856
|
+
targetCtx.beginPath();
|
|
79857
|
+
targetCtx.moveTo(x - 7, y);
|
|
79858
|
+
targetCtx.lineTo(x + 7, y);
|
|
79859
|
+
targetCtx.moveTo(x, y - 7);
|
|
79860
|
+
targetCtx.lineTo(x, y + 7);
|
|
79861
|
+
targetCtx.stroke();
|
|
79862
|
+
targetCtx.restore();
|
|
79863
|
+
}
|
|
79864
|
+
}
|
|
79865
|
+
// Lasso v2: smooth closed curve through the placed vertices (closed + filled
|
|
79866
|
+
// from ≥2 vertices), vertex dots, and a red ✕ on the hovered vertex.
|
|
79867
|
+
if (this.promptTool === "lasso" && this.lassoVerts.length > 0) {
|
|
79868
|
+
this.renderLassoOverlay(targetCtx);
|
|
79869
|
+
}
|
|
79445
79870
|
}
|
|
79446
|
-
|
|
79871
|
+
/** A seed marker (small filled dot + ring) for point prompts. */
|
|
79872
|
+
drawSeedMarker(ctx, x, y, fg) {
|
|
79873
|
+
ctx.save();
|
|
79874
|
+
const col = fg ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79875
|
+
ctx.fillStyle = col;
|
|
79876
|
+
ctx.strokeStyle = "rgba(0,0,0,0.55)";
|
|
79877
|
+
ctx.lineWidth = 1.5;
|
|
79878
|
+
ctx.beginPath();
|
|
79879
|
+
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
|
79880
|
+
ctx.fill();
|
|
79881
|
+
ctx.stroke();
|
|
79882
|
+
ctx.restore();
|
|
79883
|
+
}
|
|
79884
|
+
/** Render the lasso editing overlay (curve + fill + vertices + hover ✕). */
|
|
79885
|
+
renderLassoOverlay(ctx) {
|
|
79886
|
+
var _a;
|
|
79887
|
+
const fg = this.lassoVerts[0].label === 1;
|
|
79888
|
+
const stroke = fg ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79889
|
+
const fill = fg ? "rgba(120,255,180,0.14)" : "rgba(255,120,120,0.14)";
|
|
79890
|
+
// PATH: angularly-ordered vertices (simple, non-crossing loop) in screen space.
|
|
79891
|
+
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 }; });
|
|
79892
|
+
const path = ov.length >= 3 ? this.sampleClosedSpline(ov, 16) : ov;
|
|
79893
|
+
ctx.save();
|
|
79894
|
+
if (path.length >= 2) {
|
|
79895
|
+
ctx.beginPath();
|
|
79896
|
+
ctx.moveTo(path[0].x, path[0].y);
|
|
79897
|
+
for (let i = 1; i < path.length; i++)
|
|
79898
|
+
ctx.lineTo(path[i].x, path[i].y);
|
|
79899
|
+
ctx.closePath();
|
|
79900
|
+
ctx.fillStyle = fill;
|
|
79901
|
+
ctx.fill();
|
|
79902
|
+
ctx.lineCap = "round";
|
|
79903
|
+
ctx.lineJoin = "round";
|
|
79904
|
+
ctx.lineWidth = 1.75;
|
|
79905
|
+
ctx.strokeStyle = stroke;
|
|
79906
|
+
ctx.stroke();
|
|
79907
|
+
}
|
|
79908
|
+
// Vertex handles — drawn from the ORIGINAL vertex order so lassoHoverIdx (an
|
|
79909
|
+
// index into lassoVerts from the hit-test) marks the correct one for the ✕.
|
|
79910
|
+
for (let i = 0; i < this.lassoVerts.length; i++) {
|
|
79911
|
+
const v = this.lassoVerts[i];
|
|
79912
|
+
const p = (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy };
|
|
79913
|
+
if (i === this.lassoHoverIdx) {
|
|
79914
|
+
// Red ✕ — click to delete this vertex.
|
|
79915
|
+
ctx.strokeStyle = "rgba(255,90,90,0.98)";
|
|
79916
|
+
ctx.lineWidth = 2;
|
|
79917
|
+
ctx.beginPath();
|
|
79918
|
+
ctx.moveTo(p.x - 5, p.y - 5);
|
|
79919
|
+
ctx.lineTo(p.x + 5, p.y + 5);
|
|
79920
|
+
ctx.moveTo(p.x + 5, p.y - 5);
|
|
79921
|
+
ctx.lineTo(p.x - 5, p.y + 5);
|
|
79922
|
+
ctx.stroke();
|
|
79923
|
+
}
|
|
79924
|
+
else {
|
|
79925
|
+
ctx.fillStyle = stroke;
|
|
79926
|
+
ctx.strokeStyle = "rgba(0,0,0,0.55)";
|
|
79927
|
+
ctx.lineWidth = 1.25;
|
|
79928
|
+
ctx.beginPath();
|
|
79929
|
+
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
|
79930
|
+
ctx.fill();
|
|
79931
|
+
ctx.stroke();
|
|
79932
|
+
}
|
|
79933
|
+
}
|
|
79934
|
+
ctx.restore();
|
|
79935
|
+
}
|
|
79936
|
+
}
|
|
79937
|
+
/** Screen-pixel radius for hit-testing a hovered vertex. */
|
|
79938
|
+
AiAssistTool.LASSO_HIT_R = 8;
|
|
79447
79939
|
|
|
79448
79940
|
/**
|
|
79449
79941
|
* DrawToolCore — Tool orchestration and event routing.
|
|
@@ -82478,16 +82970,39 @@ void main() {
|
|
|
82478
82970
|
// — Driver methods called by the app-layer composable —
|
|
82479
82971
|
aiSetPromptTool(tool) { this.drawCore.aiAssistTool.setPromptTool(tool); }
|
|
82480
82972
|
aiSetPolarity(label) { this.drawCore.aiAssistTool.setPolarity(label); }
|
|
82481
|
-
/**
|
|
82482
|
-
|
|
82973
|
+
/** Select the active Segmentation by its label value (the AI paints into it). */
|
|
82974
|
+
aiSetActiveSegment(label) { this.drawCore.aiAssistTool.setActiveSegment(label); }
|
|
82975
|
+
/** Set a Segmentation's colour (its label's colorMap entry; 2D overlay repaints). */
|
|
82976
|
+
aiSetSegmentColor(label, color) {
|
|
82977
|
+
this.drawCore.aiAssistTool.setSegmentColor(label, color);
|
|
82978
|
+
}
|
|
82979
|
+
/** "New segmentation": freeze current regions + switch to a new label. */
|
|
82980
|
+
aiNewSegment(label) { this.drawCore.aiAssistTool.newSegment(label); }
|
|
82981
|
+
/** Delete a Segmentation: erase all its label's voxels from scratch + committed. */
|
|
82982
|
+
aiClearSegment(label) { this.drawCore.aiAssistTool.clearSegment(label); }
|
|
82483
82983
|
/** Set the scribble brush radius (px). */
|
|
82484
82984
|
aiSetScribbleSize(size) { this.drawCore.aiAssistTool.setScribbleSize(size); }
|
|
82485
82985
|
/** Register the callback invoked when a prompt gesture completes (app → backend). */
|
|
82486
82986
|
aiOnPrompt(cb) { this.drawCore.aiAssistTool.onPrompt = cb; }
|
|
82987
|
+
/** Register the callback fired when the lasso vertex set changes (drives the Finish button). */
|
|
82988
|
+
aiOnLassoChange(cb) { this.drawCore.aiAssistTool.onLassoChange = cb; }
|
|
82487
82989
|
/** Apply a backend mask result into the scratch volume (overlay repaints next frame). */
|
|
82488
82990
|
aiApplyMask(result) { this.drawCore.aiAssistTool.applyMask(result); }
|
|
82489
82991
|
/** Clear the in-progress prompt set (e.g. slice change). */
|
|
82490
82992
|
aiClearPrompts() { this.drawCore.aiAssistTool.resetPrompts(); }
|
|
82993
|
+
// — Lasso v2 (discrete-vertex closed-curve) controls —
|
|
82994
|
+
/** Close + send the in-progress lasso to the backend (needs ≥3 vertices). */
|
|
82995
|
+
aiFinishLasso() { this.drawCore.aiAssistTool.finishLasso(); }
|
|
82996
|
+
/** Undo the last lasso vertex add/delete. */
|
|
82997
|
+
aiLassoUndo() { this.drawCore.aiAssistTool.lassoUndoAction(); }
|
|
82998
|
+
/** Redo the last undone lasso vertex change. */
|
|
82999
|
+
aiLassoRedo() { this.drawCore.aiAssistTool.lassoRedoAction(); }
|
|
83000
|
+
/** Abandon the in-progress lasso (Esc). */
|
|
83001
|
+
aiCancelLasso() { this.drawCore.aiAssistTool.cancelLasso(); }
|
|
83002
|
+
/** True while the user is placing lasso vertices. */
|
|
83003
|
+
aiIsLassoEditing() { return this.drawCore.aiAssistTool.isLassoEditing(); }
|
|
83004
|
+
/** Number of placed lasso vertices (drives the Finish button). */
|
|
83005
|
+
aiLassoVertCount() { return this.drawCore.aiAssistTool.getLassoVertCount(); }
|
|
82491
83006
|
/** "New region": freeze current regions (they persist) + start a fresh prompt set. */
|
|
82492
83007
|
aiCommitRegion() { this.drawCore.aiAssistTool.commitRegion(); }
|
|
82493
83008
|
/** Discard all AI scratch painting since enter (sandbox discard). */
|
|
@@ -82498,6 +83013,11 @@ void main() {
|
|
|
82498
83013
|
aiGetScratchSlices() {
|
|
82499
83014
|
return this.drawCore.aiAssistTool.getScratchSlices();
|
|
82500
83015
|
}
|
|
83016
|
+
/** Per-segmentation serialization (one binary RLE per label per z-slice) for the
|
|
83017
|
+
* multi-label / per-colour 3D build. */
|
|
83018
|
+
aiGetScratchSegments() {
|
|
83019
|
+
return this.drawCore.aiAssistTool.getScratchSegments();
|
|
83020
|
+
}
|
|
82501
83021
|
/**
|
|
82502
83022
|
* Merge the AI scratch into a target layer as a single undoable group (sandbox
|
|
82503
83023
|
* merge — best-practice: non-destructive + one Ctrl+Z). The scratch's channel
|
|
@@ -82900,8 +83420,15 @@ void main() {
|
|
|
82900
83420
|
}
|
|
82901
83421
|
|
|
82902
83422
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
82903
|
-
|
|
82904
|
-
|
|
83423
|
+
// "v3.6.5" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version
|
|
83424
|
+
const REVISION = "v3.6.5";
|
|
83425
|
+
// Expose on global so the version can be read in a production browser console via window.__COPPER3D_VERSION__
|
|
83426
|
+
if (typeof window !== "undefined") {
|
|
83427
|
+
window.__COPPER3D_VERSION__ = REVISION;
|
|
83428
|
+
}
|
|
83429
|
+
if (typeof console !== "undefined") {
|
|
83430
|
+
console.log(`%cCopper3D Visualisation %c${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
|
|
83431
|
+
}
|
|
82905
83432
|
|
|
82906
83433
|
exports.AI_CHANNEL_HEX_COLORS = AI_CHANNEL_HEX_COLORS;
|
|
82907
83434
|
exports.AI_MASK_CHANNEL_COLORS = AI_MASK_CHANNEL_COLORS;
|