copper3d 3.6.8 → 3.6.10
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/Scene/copperScene.js +7 -0
- package/dist/Scene/copperScene.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.d.ts +41 -19
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.js +329 -115
- package/dist/Utils/surfaceAnnotation/SurfaceAnnotator.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/annotationStore.d.ts +3 -10
- package/dist/Utils/surfaceAnnotation/annotationStore.js +21 -10
- package/dist/Utils/surfaceAnnotation/annotationStore.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/geodesicContour.d.ts +26 -0
- package/dist/Utils/surfaceAnnotation/geodesicContour.js +101 -1
- package/dist/Utils/surfaceAnnotation/geodesicContour.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/pointMarkers.d.ts +8 -0
- package/dist/Utils/surfaceAnnotation/pointMarkers.js +19 -0
- package/dist/Utils/surfaceAnnotation/pointMarkers.js.map +1 -1
- package/dist/Utils/surfaceAnnotation/types.d.ts +6 -0
- package/dist/Utils/surfaceAnnotation/types.js.map +1 -1
- package/dist/bundle.esm.js +478 -128
- package/dist/bundle.umd.js +478 -128
- package/dist/types/Utils/surfaceAnnotation/SurfaceAnnotator.d.ts +41 -19
- package/dist/types/Utils/surfaceAnnotation/annotationStore.d.ts +3 -10
- package/dist/types/Utils/surfaceAnnotation/geodesicContour.d.ts +26 -0
- package/dist/types/Utils/surfaceAnnotation/pointMarkers.d.ts +8 -0
- package/dist/types/Utils/surfaceAnnotation/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/bundle.umd.js
CHANGED
|
@@ -59654,6 +59654,25 @@ void main() {
|
|
|
59654
59654
|
m.position.copy(p.addScaledVector(n, radius * 0.5));
|
|
59655
59655
|
m.renderOrder = 999;
|
|
59656
59656
|
return m;
|
|
59657
|
+
}
|
|
59658
|
+
/**
|
|
59659
|
+
* Create a draggable ANCHOR HANDLE (for the geodesic editor): a white outer rim around a
|
|
59660
|
+
* tool-colored core, so the handle stands out against both the (same-colored) contour line and the
|
|
59661
|
+
* grey surface. Returned as a Group; scale it to show hover/selection feedback. depthTest stays ON
|
|
59662
|
+
* (occluded by the model on the far side) and it's lifted a touch more than a plain marker so it
|
|
59663
|
+
* sits above the thickened line rather than sinking into it.
|
|
59664
|
+
*/
|
|
59665
|
+
function makeAnchorHandle(v, mesh, coreColor, radius) {
|
|
59666
|
+
const g = new Group();
|
|
59667
|
+
const rim = new Mesh(new SphereGeometry(radius, 16, 16), new MeshBasicMaterial({ color: "#ffffff" }));
|
|
59668
|
+
rim.renderOrder = 1000;
|
|
59669
|
+
const core = new Mesh(new SphereGeometry(radius * 0.6, 16, 16), new MeshBasicMaterial({ color: coreColor }));
|
|
59670
|
+
core.renderOrder = 1001;
|
|
59671
|
+
g.add(rim);
|
|
59672
|
+
g.add(core);
|
|
59673
|
+
const { p, n } = localVertexToWorld(v, mesh);
|
|
59674
|
+
g.position.copy(p.addScaledVector(n, radius * 0.7));
|
|
59675
|
+
return g;
|
|
59657
59676
|
}
|
|
59658
59677
|
|
|
59659
59678
|
/**
|
|
@@ -61281,6 +61300,60 @@ void main() {
|
|
|
61281
61300
|
}
|
|
61282
61301
|
this.anchors.push(v);
|
|
61283
61302
|
}
|
|
61303
|
+
/** Move the anchor at `index` to the vertex nearest `localHitPoint`, rebuilding adjacent segments live. */
|
|
61304
|
+
moveAnchorTo(index, localHitPoint) {
|
|
61305
|
+
if (index < 0 || index >= this.anchors.length)
|
|
61306
|
+
return;
|
|
61307
|
+
this.snapshot();
|
|
61308
|
+
this.anchors[index] = this.graph.nearestVertex(localHitPoint);
|
|
61309
|
+
this.rebuildSegments();
|
|
61310
|
+
}
|
|
61311
|
+
/** Anchor vertex indices (persisted on a committed geodesic so it can be re-opened for editing). */
|
|
61312
|
+
getAnchorIndices() {
|
|
61313
|
+
return this.anchors.slice();
|
|
61314
|
+
}
|
|
61315
|
+
/** Rebuild an editable contour from stored anchor vertex indices (re-opening a committed geodesic). */
|
|
61316
|
+
static fromAnchors(graph, mesh, indices) {
|
|
61317
|
+
const gc = new GeodesicContour(graph, mesh);
|
|
61318
|
+
gc.anchors = indices.slice();
|
|
61319
|
+
gc.rebuildSegments();
|
|
61320
|
+
return gc;
|
|
61321
|
+
}
|
|
61322
|
+
/** Insert a new anchor (snapped to the nearest vertex) at position `index`, rebuilding segments. */
|
|
61323
|
+
insertAnchorAt(index, localHitPoint) {
|
|
61324
|
+
const v = this.graph.nearestVertex(localHitPoint);
|
|
61325
|
+
this.snapshot();
|
|
61326
|
+
const i = Math.max(0, Math.min(index, this.anchors.length));
|
|
61327
|
+
this.anchors.splice(i, 0, v);
|
|
61328
|
+
this.rebuildSegments();
|
|
61329
|
+
}
|
|
61330
|
+
/**
|
|
61331
|
+
* Which anchor interval's surface path passes closest to `worldPoint` — i.e. the edge the user
|
|
61332
|
+
* clicked on, so a new anchor can be inserted there. Returns the interval index (insert after that
|
|
61333
|
+
* anchor, i.e. at index+1), or -1 when there are fewer than 2 anchors or the click is farther than
|
|
61334
|
+
* `maxDist` (world units) from every edge. For a closed contour the trailing closing edge
|
|
61335
|
+
* (last→first) is considered and maps to appending at the end.
|
|
61336
|
+
*/
|
|
61337
|
+
nearestInsertIndex(worldPoint, matrixWorld, closed, maxDist) {
|
|
61338
|
+
if (this.anchors.length < 2)
|
|
61339
|
+
return -1;
|
|
61340
|
+
const segs = this.segments.slice();
|
|
61341
|
+
if (closed && this.anchors.length > 2) {
|
|
61342
|
+
segs.push(this.graph.shortestPath(this.anchors[this.anchors.length - 1], this.anchors[0]));
|
|
61343
|
+
}
|
|
61344
|
+
let best = -1;
|
|
61345
|
+
let bestD = maxDist;
|
|
61346
|
+
for (let si = 0; si < segs.length; si++) {
|
|
61347
|
+
for (const vi of segs[si]) {
|
|
61348
|
+
const d = this.graph.vertexWorld(vi, matrixWorld).distanceTo(worldPoint);
|
|
61349
|
+
if (d < bestD) {
|
|
61350
|
+
bestD = d;
|
|
61351
|
+
best = si;
|
|
61352
|
+
}
|
|
61353
|
+
}
|
|
61354
|
+
}
|
|
61355
|
+
return best;
|
|
61356
|
+
}
|
|
61284
61357
|
/** Remove the anchor at the given index and recompute adjacent segments (allows canceling any middle point). */
|
|
61285
61358
|
removeAnchorAt(index) {
|
|
61286
61359
|
if (index < 0 || index >= this.anchors.length)
|
|
@@ -61332,7 +61405,52 @@ void main() {
|
|
|
61332
61405
|
idxPath.push(this.anchors[0]);
|
|
61333
61406
|
}
|
|
61334
61407
|
// The graph geometry is already local space, so emit local vertices directly (world is derived at render time).
|
|
61335
|
-
return
|
|
61408
|
+
return this.smoothPath(idxPath, closed);
|
|
61409
|
+
}
|
|
61410
|
+
/**
|
|
61411
|
+
* Relax the stitched shortest-path polyline so it reads as a smooth curve instead of the raw
|
|
61412
|
+
* voxel staircase, WITHOUT changing the point count (keeps editing/index mapping simple) and
|
|
61413
|
+
* WITHOUT moving the anchors (they stay exact control points). A few Laplacian passes pull each
|
|
61414
|
+
* interior point toward the midpoint of its neighbours; anchors and (for open paths) the two
|
|
61415
|
+
* endpoints are pinned. Normals are carried over from each point's original graph vertex — the
|
|
61416
|
+
* points only shift slightly and the line already floats above the surface by `epsilon`, so the
|
|
61417
|
+
* approximation is invisible and avoids an O(V) nearest-vertex scan per point on every redraw.
|
|
61418
|
+
*/
|
|
61419
|
+
smoothPath(idxPath, closed) {
|
|
61420
|
+
const base = idxPath.map((i) => this.graph.vertexLocal(i));
|
|
61421
|
+
const n = base.length;
|
|
61422
|
+
if (n <= 2)
|
|
61423
|
+
return base;
|
|
61424
|
+
const anchorSet = new Set(this.anchors);
|
|
61425
|
+
const pinned = idxPath.map((i) => anchorSet.has(i));
|
|
61426
|
+
const P = base.map((v) => new Vector3(v.x, v.y, v.z));
|
|
61427
|
+
const ITER = 2;
|
|
61428
|
+
const LAMBDA = 0.5;
|
|
61429
|
+
const mid = new Vector3();
|
|
61430
|
+
for (let it = 0; it < ITER; it++) {
|
|
61431
|
+
const next = P.map((p) => p.clone());
|
|
61432
|
+
for (let k = 0; k < n; k++) {
|
|
61433
|
+
if (pinned[k])
|
|
61434
|
+
continue;
|
|
61435
|
+
if (!closed && (k === 0 || k === n - 1))
|
|
61436
|
+
continue;
|
|
61437
|
+
const a = P[(k - 1 + n) % n];
|
|
61438
|
+
const b = P[(k + 1) % n];
|
|
61439
|
+
mid.addVectors(a, b).multiplyScalar(0.5).sub(P[k]);
|
|
61440
|
+
next[k].copy(P[k]).addScaledVector(mid, LAMBDA);
|
|
61441
|
+
}
|
|
61442
|
+
for (let k = 0; k < n; k++)
|
|
61443
|
+
P[k].copy(next[k]);
|
|
61444
|
+
}
|
|
61445
|
+
return P.map((p, k) => ({
|
|
61446
|
+
x: p.x,
|
|
61447
|
+
y: p.y,
|
|
61448
|
+
z: p.z,
|
|
61449
|
+
nx: base[k].nx,
|
|
61450
|
+
ny: base[k].ny,
|
|
61451
|
+
nz: base[k].nz,
|
|
61452
|
+
faceIndex: -1,
|
|
61453
|
+
}));
|
|
61336
61454
|
}
|
|
61337
61455
|
}
|
|
61338
61456
|
|
|
@@ -61417,6 +61535,10 @@ void main() {
|
|
|
61417
61535
|
this.notify();
|
|
61418
61536
|
}
|
|
61419
61537
|
}
|
|
61538
|
+
/** Notify subscribers after an in-place mutation of an existing annotation (e.g. geodesic edit re-commit). */
|
|
61539
|
+
touch() {
|
|
61540
|
+
this.notify();
|
|
61541
|
+
}
|
|
61420
61542
|
toJSON(modelName, mesh, opts = {}) {
|
|
61421
61543
|
var _a;
|
|
61422
61544
|
const space = (_a = opts.space) !== null && _a !== void 0 ? _a : "local";
|
|
@@ -61434,16 +61556,23 @@ void main() {
|
|
|
61434
61556
|
model: modelName,
|
|
61435
61557
|
exportedAt: new Date().toISOString(),
|
|
61436
61558
|
space,
|
|
61437
|
-
annotations: this.items.map((a) =>
|
|
61438
|
-
|
|
61439
|
-
|
|
61440
|
-
|
|
61441
|
-
|
|
61442
|
-
|
|
61443
|
-
|
|
61444
|
-
|
|
61445
|
-
|
|
61446
|
-
|
|
61559
|
+
annotations: this.items.map((a) => {
|
|
61560
|
+
const out = {
|
|
61561
|
+
id: a.id,
|
|
61562
|
+
type: a.type,
|
|
61563
|
+
mode: a.mode,
|
|
61564
|
+
label: a.label,
|
|
61565
|
+
color: a.color,
|
|
61566
|
+
closed: a.closed,
|
|
61567
|
+
visible: a.visible,
|
|
61568
|
+
points: a.vertices.map(toPt),
|
|
61569
|
+
};
|
|
61570
|
+
// Geodesic contours also carry their control points so a round-tripped import stays editable.
|
|
61571
|
+
if (a.mode === "geodesic" && a.anchors && a.anchors.length) {
|
|
61572
|
+
out.anchors = a.anchors.map(toPt);
|
|
61573
|
+
}
|
|
61574
|
+
return out;
|
|
61575
|
+
}),
|
|
61447
61576
|
};
|
|
61448
61577
|
}
|
|
61449
61578
|
}
|
|
@@ -61470,11 +61599,20 @@ void main() {
|
|
|
61470
61599
|
this.managed = new Set();
|
|
61471
61600
|
this.seq = 0;
|
|
61472
61601
|
this.selectedId = null;
|
|
61473
|
-
this.activeGeoMarkers = []; // visible
|
|
61602
|
+
this.activeGeoMarkers = []; // visible anchor handles of the active geodesic
|
|
61474
61603
|
this.geoRay = new Raycaster();
|
|
61475
61604
|
this.geoNdc = new Vector2();
|
|
61476
61605
|
this.hoveredGeoMarker = -1;
|
|
61606
|
+
this.draggingAnchor = -1; // anchor index currently being dragged (-1 = none)
|
|
61607
|
+
this.geoRedrawScheduled = false; // rAF coalescing flag for live drag redraw
|
|
61608
|
+
this.editingId = null; // committed geodesic re-opened for editing (Task 1.5)
|
|
61609
|
+
this.geoClosed = false; // whether the active/edited geodesic is a closed loop
|
|
61477
61610
|
this._projV = new Vector3();
|
|
61611
|
+
/** Block the OS right-click menu when the pointer is over the WebGL canvas (not the HTML panels). */
|
|
61612
|
+
this.onContextMenu = (e) => {
|
|
61613
|
+
if (this.insideContainer(e))
|
|
61614
|
+
e.preventDefault();
|
|
61615
|
+
};
|
|
61478
61616
|
/** Update the pixel resolution of all fat lines on window resize, otherwise line width distorts. */
|
|
61479
61617
|
this.onResize = () => {
|
|
61480
61618
|
const w = this.o.container.clientWidth;
|
|
@@ -61491,10 +61629,50 @@ void main() {
|
|
|
61491
61629
|
(this.activeGeoLine.material.resolution.set(w, h));
|
|
61492
61630
|
};
|
|
61493
61631
|
this.onPointerDown = (e) => {
|
|
61494
|
-
if (e.button !== 0)
|
|
61495
|
-
return; // left button only
|
|
61496
61632
|
if (!this.insideContainer(e))
|
|
61497
61633
|
return;
|
|
61634
|
+
// Geodesic anchor editing (drag to move / right-click to delete) is available whenever an
|
|
61635
|
+
// editable geodesic exists — even outside draw-lock — because it requires precisely hitting an
|
|
61636
|
+
// existing anchor. This lets the user rotate the model freely and still grab a point. Handled
|
|
61637
|
+
// BEFORE the button/drawing gates below.
|
|
61638
|
+
if (this.armed === "geodesic" && this.activeGeo) {
|
|
61639
|
+
if (e.button === 2) {
|
|
61640
|
+
const pick = this.pickGeoMarker(e);
|
|
61641
|
+
if (pick >= 0) {
|
|
61642
|
+
e.preventDefault();
|
|
61643
|
+
this.activeGeo.removeAnchorAt(pick);
|
|
61644
|
+
this.afterGeoEdit();
|
|
61645
|
+
return;
|
|
61646
|
+
}
|
|
61647
|
+
}
|
|
61648
|
+
else if (e.button === 0) {
|
|
61649
|
+
const pick = this.pickGeoMarker(e);
|
|
61650
|
+
if (pick >= 0) {
|
|
61651
|
+
e.preventDefault();
|
|
61652
|
+
this.draggingAnchor = pick;
|
|
61653
|
+
this.pointerDown = true;
|
|
61654
|
+
this.o.controls.enabled = false; // suppress camera while dragging the anchor
|
|
61655
|
+
this.o.container.style.cursor = "grabbing";
|
|
61656
|
+
return;
|
|
61657
|
+
}
|
|
61658
|
+
// Missed every anchor → if the click lands on the line body, insert a new anchor there
|
|
61659
|
+
// (works in an edit session or an in-progress geodesic, regardless of draw-lock).
|
|
61660
|
+
if (this.activeGeo.anchorCount >= 2) {
|
|
61661
|
+
const h = this.hit(e);
|
|
61662
|
+
if (h) {
|
|
61663
|
+
const insertAt = this.activeGeo.nearestInsertIndex(h.point, this.o.mesh.matrixWorld, this.geoClosed, this.minGap * 4);
|
|
61664
|
+
if (insertAt >= 0) {
|
|
61665
|
+
e.preventDefault();
|
|
61666
|
+
this.activeGeo.insertAnchorAt(insertAt + 1, this.o.mesh.worldToLocal(h.point.clone()));
|
|
61667
|
+
this.afterGeoEdit();
|
|
61668
|
+
return;
|
|
61669
|
+
}
|
|
61670
|
+
}
|
|
61671
|
+
}
|
|
61672
|
+
}
|
|
61673
|
+
}
|
|
61674
|
+
if (e.button !== 0)
|
|
61675
|
+
return; // the remaining tools are left-button only
|
|
61498
61676
|
this.pointerDown = true;
|
|
61499
61677
|
// Only act with the armed tool when in drawing mode (drawLock or spaceHeld).
|
|
61500
61678
|
// When not drawing, navigation is the default — pointer events go to camera controls.
|
|
@@ -61534,19 +61712,8 @@ void main() {
|
|
|
61534
61712
|
return;
|
|
61535
61713
|
}
|
|
61536
61714
|
if (activeTool === "geodesic") {
|
|
61537
|
-
//
|
|
61538
|
-
|
|
61539
|
-
if (pick >= 0 && this.activeGeo) {
|
|
61540
|
-
this.activeGeo.removeAnchorAt(pick);
|
|
61541
|
-
if (this.activeGeo.anchorCount === 0)
|
|
61542
|
-
this.clearActiveGeo();
|
|
61543
|
-
else {
|
|
61544
|
-
this.rebuildGeoMarkers();
|
|
61545
|
-
this.redrawGeoLine();
|
|
61546
|
-
}
|
|
61547
|
-
return;
|
|
61548
|
-
}
|
|
61549
|
-
// Otherwise drop a new anchor on the surface.
|
|
61715
|
+
// Anchor grab/delete was already handled above; here we only drop a NEW anchor on the surface
|
|
61716
|
+
// (clicks on an existing anchor never reach this point).
|
|
61550
61717
|
const h = this.hit(e);
|
|
61551
61718
|
if (!h)
|
|
61552
61719
|
return;
|
|
@@ -61555,11 +61722,20 @@ void main() {
|
|
|
61555
61722
|
}
|
|
61556
61723
|
const local = this.o.mesh.worldToLocal(h.point.clone());
|
|
61557
61724
|
this.activeGeo.addAnchor(local);
|
|
61558
|
-
this.
|
|
61559
|
-
this.redrawGeoLine();
|
|
61725
|
+
this.afterGeoEdit();
|
|
61560
61726
|
}
|
|
61561
61727
|
};
|
|
61562
61728
|
this.onPointerMove = (e) => {
|
|
61729
|
+
// Live anchor drag: move the grabbed anchor to the surface point under the cursor and redraw
|
|
61730
|
+
// (both adjacent segments recompute inside moveAnchorTo). Takes priority over everything else.
|
|
61731
|
+
if (this.draggingAnchor >= 0) {
|
|
61732
|
+
const h = this.hit(e);
|
|
61733
|
+
if (h && this.activeGeo) {
|
|
61734
|
+
this.activeGeo.moveAnchorTo(this.draggingAnchor, this.o.mesh.worldToLocal(h.point.clone()));
|
|
61735
|
+
this.scheduleGeoRedraw();
|
|
61736
|
+
}
|
|
61737
|
+
return;
|
|
61738
|
+
}
|
|
61563
61739
|
// Track if user dragged while space was held (so a hold-drag is not treated as a tap).
|
|
61564
61740
|
if (this.spaceHeld && this.pointerDown) {
|
|
61565
61741
|
this.spaceDragged = true;
|
|
@@ -61591,6 +61767,15 @@ void main() {
|
|
|
61591
61767
|
}
|
|
61592
61768
|
};
|
|
61593
61769
|
this.onPointerUp = () => {
|
|
61770
|
+
// Finish an anchor drag: restore camera gating + cursor. (Re-commit of an edited committed
|
|
61771
|
+
// contour is wired in Task 1.5.)
|
|
61772
|
+
if (this.draggingAnchor >= 0) {
|
|
61773
|
+
this.draggingAnchor = -1;
|
|
61774
|
+
this.pointerDown = false;
|
|
61775
|
+
this.o.container.style.cursor = this.hoveredGeoMarker >= 0 ? "grab" : "";
|
|
61776
|
+
this.applyCameraGating();
|
|
61777
|
+
return;
|
|
61778
|
+
}
|
|
61594
61779
|
if (!this.pointerDown)
|
|
61595
61780
|
return;
|
|
61596
61781
|
this.pointerDown = false;
|
|
@@ -61632,6 +61817,18 @@ void main() {
|
|
|
61632
61817
|
return;
|
|
61633
61818
|
}
|
|
61634
61819
|
if (e.key === "Escape") {
|
|
61820
|
+
// First Esc clears an active selection / in-progress geodesic while staying in the current
|
|
61821
|
+
// tool; a second Esc (nothing selected) returns to Navigate.
|
|
61822
|
+
if (this.selectedId || this.activeGeo) {
|
|
61823
|
+
if (this.editingId)
|
|
61824
|
+
this.recommitGeodesic();
|
|
61825
|
+
else if (this.activeGeo)
|
|
61826
|
+
this.clearActiveGeo();
|
|
61827
|
+
this.setSelected(null);
|
|
61828
|
+
this.drawLock = false;
|
|
61829
|
+
this.applyCameraGating();
|
|
61830
|
+
return;
|
|
61831
|
+
}
|
|
61635
61832
|
this.drawLock = false;
|
|
61636
61833
|
this.setMode("navigate");
|
|
61637
61834
|
return;
|
|
@@ -61647,8 +61844,11 @@ void main() {
|
|
|
61647
61844
|
return;
|
|
61648
61845
|
}
|
|
61649
61846
|
if (e.key === "Enter") {
|
|
61650
|
-
//
|
|
61651
|
-
|
|
61847
|
+
// Editing a committed geodesic → finalize the edit; a fresh geodesic in progress → finish and
|
|
61848
|
+
// close it; otherwise close the most recent freehand line.
|
|
61849
|
+
if (this.editingId)
|
|
61850
|
+
this.recommitGeodesic();
|
|
61851
|
+
else if (this.activeGeo)
|
|
61652
61852
|
this.finishGeodesic();
|
|
61653
61853
|
else
|
|
61654
61854
|
this.closeLastContour();
|
|
@@ -61701,6 +61901,10 @@ void main() {
|
|
|
61701
61901
|
window.addEventListener("keydown", this.onKeyDown);
|
|
61702
61902
|
window.addEventListener("keyup", this.onKeyUp);
|
|
61703
61903
|
window.addEventListener("resize", this.onResize);
|
|
61904
|
+
// Suppress the browser's native context menu over the WebGL canvas in EVERY mode: right-click
|
|
61905
|
+
// there is used for camera pan (and, in geodesic mode, delete-anchor), so the OS menu must
|
|
61906
|
+
// never appear. Guarded by insideContainer → right-clicking the HTML panels keeps its menu.
|
|
61907
|
+
window.addEventListener("contextmenu", this.onContextMenu, true);
|
|
61704
61908
|
this.applyCameraGating();
|
|
61705
61909
|
}
|
|
61706
61910
|
get freehandColor() {
|
|
@@ -61721,14 +61925,22 @@ void main() {
|
|
|
61721
61925
|
}
|
|
61722
61926
|
setMode(m) {
|
|
61723
61927
|
var _a, _b;
|
|
61724
|
-
// When leaving geodesic mode
|
|
61725
|
-
|
|
61726
|
-
|
|
61928
|
+
// When leaving geodesic mode: commit an in-progress EDIT of a committed contour, or discard a
|
|
61929
|
+
// not-yet-committed new geodesic (clear leftover anchors and line).
|
|
61930
|
+
if (m !== "geodesic") {
|
|
61931
|
+
if (this.editingId)
|
|
61932
|
+
this.recommitGeodesic();
|
|
61933
|
+
else if (this.activeGeo)
|
|
61934
|
+
this.clearActiveGeo();
|
|
61935
|
+
}
|
|
61727
61936
|
this.mode = m;
|
|
61728
61937
|
// Record armed tool when choosing a drawing mode (not navigate).
|
|
61729
61938
|
// Camera gating is NOT changed here — the user must use Space to enter drawing.
|
|
61730
61939
|
if (m !== "navigate")
|
|
61731
61940
|
this.armed = m;
|
|
61941
|
+
// Returning to Navigate clears the selection too (put everything away).
|
|
61942
|
+
if (m === "navigate")
|
|
61943
|
+
this.setSelected(null);
|
|
61732
61944
|
this.applyCameraGating();
|
|
61733
61945
|
(_b = (_a = this.o).onModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, m);
|
|
61734
61946
|
}
|
|
@@ -61740,21 +61952,23 @@ void main() {
|
|
|
61740
61952
|
return this.store.list();
|
|
61741
61953
|
}
|
|
61742
61954
|
undo() {
|
|
61743
|
-
//
|
|
61744
|
-
//
|
|
61955
|
+
// A geodesic being drawn/edited → undo the most recent anchor edit. afterGeoEdit() handles both
|
|
61956
|
+
// the in-progress and committed-edit cases uniformly, including degenerate cleanup (so undo can't
|
|
61957
|
+
// dispose a committed line or leave a stale edit session).
|
|
61745
61958
|
if (this.activeGeo && this.activeGeo.canUndo()) {
|
|
61746
61959
|
this.activeGeo.undoEdit();
|
|
61747
|
-
|
|
61748
|
-
this.clearActiveGeo();
|
|
61749
|
-
else {
|
|
61750
|
-
this.rebuildGeoMarkers();
|
|
61751
|
-
this.redrawGeoLine();
|
|
61752
|
-
}
|
|
61960
|
+
this.afterGeoEdit();
|
|
61753
61961
|
return;
|
|
61754
61962
|
}
|
|
61755
61963
|
this.store.undo();
|
|
61756
61964
|
}
|
|
61757
61965
|
clearAll() {
|
|
61966
|
+
// Drop any edit session first; the edited line is owned by the store and disposed by clear() below.
|
|
61967
|
+
if (this.editingId) {
|
|
61968
|
+
this.activeGeoLine = undefined;
|
|
61969
|
+
this.editingId = null;
|
|
61970
|
+
this.geoClosed = false;
|
|
61971
|
+
}
|
|
61758
61972
|
const removed = this.store.clear();
|
|
61759
61973
|
removed.forEach((a) => {
|
|
61760
61974
|
if (a.object3D) {
|
|
@@ -61767,13 +61981,49 @@ void main() {
|
|
|
61767
61981
|
this.clearActiveGeo();
|
|
61768
61982
|
}
|
|
61769
61983
|
deleteAnnotation(id) {
|
|
61984
|
+
// If we're deleting the geodesic currently being edited, tear the edit session down first so
|
|
61985
|
+
// its anchor handles aren't orphaned in the scene (the line itself is removed by reconcile).
|
|
61986
|
+
if (this.editingId === id) {
|
|
61987
|
+
this.removeGeoMarkers();
|
|
61988
|
+
this.activeGeo = undefined;
|
|
61989
|
+
this.activeGeoLine = undefined;
|
|
61990
|
+
this.editingId = null;
|
|
61991
|
+
this.geoClosed = false;
|
|
61992
|
+
}
|
|
61770
61993
|
if (this.selectedId === id)
|
|
61771
|
-
this.
|
|
61994
|
+
this.setSelected(null);
|
|
61772
61995
|
this.store.remove(id);
|
|
61996
|
+
this.emitInteraction();
|
|
61773
61997
|
}
|
|
61774
61998
|
selectAnnotation(id) {
|
|
61999
|
+
var _a;
|
|
62000
|
+
// Commit any open geodesic edit before the selection changes.
|
|
62001
|
+
if (this.editingId && this.editingId !== id)
|
|
62002
|
+
this.recommitGeodesic();
|
|
61775
62003
|
this.selectedId = id;
|
|
61776
62004
|
this.applySelection();
|
|
62005
|
+
if (!id)
|
|
62006
|
+
return;
|
|
62007
|
+
const a = this.store.get(id);
|
|
62008
|
+
if (!a)
|
|
62009
|
+
return;
|
|
62010
|
+
// Selecting an annotation switches to the tool that drew it, so you can carry on editing it
|
|
62011
|
+
// (and a geodesic opens its anchor handles). points → Place point; contour → its draw mode.
|
|
62012
|
+
const tool = a.type === "points" ? "point" : (_a = a.mode) !== null && _a !== void 0 ? _a : "freehand";
|
|
62013
|
+
if (this.mode !== tool)
|
|
62014
|
+
this.setMode(tool);
|
|
62015
|
+
// With Geodesic now active, re-open the contour's anchors for editing.
|
|
62016
|
+
if (this.mode === "geodesic")
|
|
62017
|
+
this.tryOpenGeoEdit(id);
|
|
62018
|
+
}
|
|
62019
|
+
/** Change the selection from inside the engine (delete / deselect-on-navigate) and mirror it to the UI. */
|
|
62020
|
+
setSelected(id) {
|
|
62021
|
+
var _a, _b;
|
|
62022
|
+
if (this.selectedId === id)
|
|
62023
|
+
return;
|
|
62024
|
+
this.selectedId = id;
|
|
62025
|
+
this.applySelection();
|
|
62026
|
+
(_b = (_a = this.o).onSelectionChange) === null || _b === void 0 ? void 0 : _b.call(_a, id);
|
|
61777
62027
|
}
|
|
61778
62028
|
/** Redraw the corresponding three object after a color change. */
|
|
61779
62029
|
refreshAnnotation(id) {
|
|
@@ -61804,17 +62054,11 @@ void main() {
|
|
|
61804
62054
|
let count = 0;
|
|
61805
62055
|
let maxImported = this.seq;
|
|
61806
62056
|
for (const a of (_a = payload.annotations) !== null && _a !== void 0 ? _a : []) {
|
|
61807
|
-
const verts = a.points.map((p) =>
|
|
61808
|
-
|
|
61809
|
-
|
|
61810
|
-
|
|
61811
|
-
|
|
61812
|
-
nx = nrm.x;
|
|
61813
|
-
ny = nrm.y;
|
|
61814
|
-
nz = nrm.z;
|
|
61815
|
-
}
|
|
61816
|
-
return { x, y, z, nx, ny, nz, faceIndex: 0 };
|
|
61817
|
-
});
|
|
62057
|
+
const verts = a.points.map((p) => this.rawToVertex(p));
|
|
62058
|
+
// Geodesic control points, if the payload carries them → keep the contour editable.
|
|
62059
|
+
const anchors = a.mode === "geodesic" && a.anchors && a.anchors.length
|
|
62060
|
+
? a.anchors.map((p) => this.rawToVertex(p))
|
|
62061
|
+
: undefined;
|
|
61818
62062
|
if (a.type === "points") {
|
|
61819
62063
|
for (const v of verts) {
|
|
61820
62064
|
// Only reuse the provided id for a single-point entry (the round-trip case);
|
|
@@ -61855,6 +62099,7 @@ void main() {
|
|
|
61855
62099
|
closed: a.closed,
|
|
61856
62100
|
visible: (_d = a.visible) !== null && _d !== void 0 ? _d : true,
|
|
61857
62101
|
vertices: verts,
|
|
62102
|
+
anchors,
|
|
61858
62103
|
object3D: line,
|
|
61859
62104
|
});
|
|
61860
62105
|
const m = id.match(/^a(\d+)$/);
|
|
@@ -61873,10 +62118,19 @@ void main() {
|
|
|
61873
62118
|
return this.drawLock || this.spaceHeld;
|
|
61874
62119
|
}
|
|
61875
62120
|
applyCameraGating() {
|
|
61876
|
-
var _a, _b;
|
|
61877
62121
|
// Default is navigate (camera enabled). Drawing only when drawLock or spaceHeld.
|
|
61878
62122
|
this.o.controls.enabled = !this.drawing;
|
|
61879
|
-
|
|
62123
|
+
this.emitInteraction();
|
|
62124
|
+
}
|
|
62125
|
+
/** Emit the current interaction state (camera gating + whether a geodesic is editable). */
|
|
62126
|
+
emitInteraction() {
|
|
62127
|
+
var _a, _b;
|
|
62128
|
+
(_b = (_a = this.o).onInteractionChange) === null || _b === void 0 ? void 0 : _b.call(_a, {
|
|
62129
|
+
drawing: this.drawing,
|
|
62130
|
+
armed: this.armed,
|
|
62131
|
+
locked: this.drawLock,
|
|
62132
|
+
editing: !!this.activeGeo,
|
|
62133
|
+
});
|
|
61880
62134
|
}
|
|
61881
62135
|
/** Reconcile the scene against store.list(): add missing objects, remove deleted ones (no dispose, kept for undo restore). */
|
|
61882
62136
|
reconcile() {
|
|
@@ -61919,18 +62173,33 @@ void main() {
|
|
|
61919
62173
|
}
|
|
61920
62174
|
}
|
|
61921
62175
|
disposeObject(o) {
|
|
61922
|
-
|
|
61923
|
-
|
|
61924
|
-
|
|
61925
|
-
|
|
61926
|
-
|
|
61927
|
-
mat
|
|
61928
|
-
|
|
61929
|
-
|
|
62176
|
+
// Traverse so composite handles (Group of rim + core meshes) are fully freed, not just the root.
|
|
62177
|
+
o.traverse((c) => {
|
|
62178
|
+
var _a, _b, _c;
|
|
62179
|
+
const any = c;
|
|
62180
|
+
(_b = (_a = any.geometry) === null || _a === void 0 ? void 0 : _a.dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
62181
|
+
const mat = any.material;
|
|
62182
|
+
if (Array.isArray(mat))
|
|
62183
|
+
mat.forEach((m) => m.dispose());
|
|
62184
|
+
else
|
|
62185
|
+
(_c = mat === null || mat === void 0 ? void 0 : mat.dispose) === null || _c === void 0 ? void 0 : _c.call(mat);
|
|
62186
|
+
});
|
|
61930
62187
|
}
|
|
61931
62188
|
nextId() {
|
|
61932
62189
|
return "a" + ++this.seq;
|
|
61933
62190
|
}
|
|
62191
|
+
/** Parse a raw exported point ([x,y,z] or [x,y,z,nx,ny,nz], local space) into an AnnotationVertex, recovering the normal from the nearest graph vertex when absent. */
|
|
62192
|
+
rawToVertex(p) {
|
|
62193
|
+
const [x, y, z] = p;
|
|
62194
|
+
let nx = p[3], ny = p[4], nz = p[5];
|
|
62195
|
+
if (nx === undefined || ny === undefined || nz === undefined) {
|
|
62196
|
+
const nrm = this.graph.nearestNormalLocal(new Vector3(x, y, z));
|
|
62197
|
+
nx = nrm.x;
|
|
62198
|
+
ny = nrm.y;
|
|
62199
|
+
nz = nrm.z;
|
|
62200
|
+
}
|
|
62201
|
+
return { x, y, z, nx, ny, nz, faceIndex: 0 };
|
|
62202
|
+
}
|
|
61934
62203
|
hit(e) {
|
|
61935
62204
|
return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
|
|
61936
62205
|
}
|
|
@@ -61945,6 +62214,55 @@ void main() {
|
|
|
61945
62214
|
const t = e.target;
|
|
61946
62215
|
return !!t && t.tagName === "CANVAS" && this.o.container.contains(t);
|
|
61947
62216
|
}
|
|
62217
|
+
/** After any anchor edit: refresh markers + line; drop/delete the contour when it becomes degenerate. */
|
|
62218
|
+
afterGeoEdit() {
|
|
62219
|
+
if (!this.activeGeo)
|
|
62220
|
+
return;
|
|
62221
|
+
if (this.editingId) {
|
|
62222
|
+
// Editing a committed contour: fewer than 2 anchors → delete the whole annotation.
|
|
62223
|
+
if (this.activeGeo.anchorCount < 2) {
|
|
62224
|
+
const id = this.editingId;
|
|
62225
|
+
this.removeGeoMarkers();
|
|
62226
|
+
this.activeGeo = undefined;
|
|
62227
|
+
this.activeGeoLine = undefined; // owned by the annotation; deleteAnnotation disposes it
|
|
62228
|
+
this.editingId = null;
|
|
62229
|
+
this.geoClosed = false;
|
|
62230
|
+
this.deleteAnnotation(id);
|
|
62231
|
+
this.emitInteraction();
|
|
62232
|
+
return;
|
|
62233
|
+
}
|
|
62234
|
+
this.rebuildGeoMarkers();
|
|
62235
|
+
this.redrawGeoLine();
|
|
62236
|
+
this.emitInteraction();
|
|
62237
|
+
return;
|
|
62238
|
+
}
|
|
62239
|
+
// In-progress new geodesic.
|
|
62240
|
+
if (this.activeGeo.anchorCount === 0) {
|
|
62241
|
+
this.clearActiveGeo();
|
|
62242
|
+
}
|
|
62243
|
+
else {
|
|
62244
|
+
this.rebuildGeoMarkers();
|
|
62245
|
+
this.redrawGeoLine();
|
|
62246
|
+
}
|
|
62247
|
+
this.emitInteraction();
|
|
62248
|
+
}
|
|
62249
|
+
/** Coalesce live drag redraws to one per animation frame (moveAnchorTo runs Dijkstra per move). */
|
|
62250
|
+
scheduleGeoRedraw() {
|
|
62251
|
+
if (this.geoRedrawScheduled)
|
|
62252
|
+
return;
|
|
62253
|
+
this.geoRedrawScheduled = true;
|
|
62254
|
+
requestAnimationFrame(() => {
|
|
62255
|
+
var _a;
|
|
62256
|
+
this.geoRedrawScheduled = false;
|
|
62257
|
+
this.rebuildGeoMarkers();
|
|
62258
|
+
this.redrawGeoLine();
|
|
62259
|
+
if (this.draggingAnchor >= 0) {
|
|
62260
|
+
this.o.container.style.cursor = "grabbing";
|
|
62261
|
+
// keep the grabbed handle emphasized through the live rebuilds
|
|
62262
|
+
(_a = this.activeGeoMarkers[this.draggingAnchor]) === null || _a === void 0 ? void 0 : _a.scale.setScalar(1.4);
|
|
62263
|
+
}
|
|
62264
|
+
});
|
|
62265
|
+
}
|
|
61948
62266
|
/**
|
|
61949
62267
|
* Pick an in-progress anchor: return the index of the nearest anchor (screen pixel distance <
|
|
61950
62268
|
* tolerance), or -1 on a miss.
|
|
@@ -61981,71 +62299,30 @@ void main() {
|
|
|
61981
62299
|
}
|
|
61982
62300
|
return best;
|
|
61983
62301
|
}
|
|
61984
|
-
/**
|
|
62302
|
+
/**
|
|
62303
|
+
* Set the currently hovered anchor (-1 = none): scale it up as a highlight and show a "grab"
|
|
62304
|
+
* cursor to signal it's draggable. Delete is now a right-click (no floating ✕ affordance).
|
|
62305
|
+
*/
|
|
61985
62306
|
setGeoHover(idx) {
|
|
61986
|
-
if (idx === this.hoveredGeoMarker)
|
|
61987
|
-
if (idx >= 0)
|
|
61988
|
-
this.positionGeoBadge(this.activeGeoMarkers[idx]);
|
|
62307
|
+
if (idx === this.hoveredGeoMarker)
|
|
61989
62308
|
return;
|
|
61990
|
-
}
|
|
61991
62309
|
const prev = this.activeGeoMarkers[this.hoveredGeoMarker];
|
|
61992
62310
|
if (prev)
|
|
61993
62311
|
prev.scale.setScalar(1);
|
|
61994
62312
|
this.hoveredGeoMarker = idx;
|
|
61995
62313
|
const cur = this.activeGeoMarkers[idx];
|
|
62314
|
+
// Don't fight the "grabbing" cursor set during an active drag.
|
|
62315
|
+
if (this.draggingAnchor >= 0)
|
|
62316
|
+
return;
|
|
61996
62317
|
if (cur) {
|
|
61997
|
-
cur.scale.setScalar(1.
|
|
61998
|
-
this.
|
|
61999
|
-
this.positionGeoBadge(cur);
|
|
62000
|
-
this.o.container.style.cursor = "pointer";
|
|
62318
|
+
cur.scale.setScalar(1.35);
|
|
62319
|
+
this.o.container.style.cursor = "grab";
|
|
62001
62320
|
}
|
|
62002
62321
|
else {
|
|
62003
|
-
if (this.geoHoverBadge)
|
|
62004
|
-
this.geoHoverBadge.style.display = "none";
|
|
62005
62322
|
this.o.container.style.cursor = "";
|
|
62006
62323
|
}
|
|
62007
62324
|
}
|
|
62008
|
-
/**
|
|
62009
|
-
ensureGeoBadge() {
|
|
62010
|
-
if (this.geoHoverBadge)
|
|
62011
|
-
return this.geoHoverBadge;
|
|
62012
|
-
const b = document.createElement("div");
|
|
62013
|
-
b.textContent = "✕";
|
|
62014
|
-
Object.assign(b.style, {
|
|
62015
|
-
position: "absolute",
|
|
62016
|
-
width: "16px",
|
|
62017
|
-
height: "16px",
|
|
62018
|
-
borderRadius: "50%",
|
|
62019
|
-
background: "#ff5d6c",
|
|
62020
|
-
color: "#fff",
|
|
62021
|
-
font: "700 10px/1 system-ui, sans-serif",
|
|
62022
|
-
display: "none",
|
|
62023
|
-
alignItems: "center",
|
|
62024
|
-
justifyContent: "center",
|
|
62025
|
-
pointerEvents: "none",
|
|
62026
|
-
zIndex: "15",
|
|
62027
|
-
boxShadow: "0 2px 6px rgba(0,0,0,.45)",
|
|
62028
|
-
transform: "translate(-50%, -50%)",
|
|
62029
|
-
left: "0px",
|
|
62030
|
-
top: "0px",
|
|
62031
|
-
});
|
|
62032
|
-
this.o.container.appendChild(b);
|
|
62033
|
-
this.geoHoverBadge = b;
|
|
62034
|
-
return b;
|
|
62035
|
-
}
|
|
62036
|
-
/** Position the ✕ badge at the anchor's screen projection (dead center, i.e. where the cursor hovers, to avoid ambiguity). */
|
|
62037
|
-
positionGeoBadge(marker) {
|
|
62038
|
-
if (!marker)
|
|
62039
|
-
return;
|
|
62040
|
-
const b = this.ensureGeoBadge();
|
|
62041
|
-
this._projV.copy(marker.position).project(this.o.camera);
|
|
62042
|
-
const rect = this.o.container.getBoundingClientRect();
|
|
62043
|
-
const x = (this._projV.x * 0.5 + 0.5) * rect.width;
|
|
62044
|
-
const y = (-this._projV.y * 0.5 + 0.5) * rect.height;
|
|
62045
|
-
b.style.left = `${x}px`;
|
|
62046
|
-
b.style.top = `${y}px`;
|
|
62047
|
-
}
|
|
62048
|
-
/** Rebuild the visible anchor spheres from the current anchors (slightly larger than placed points, so they're easy to see and click to cancel). */
|
|
62325
|
+
/** Rebuild the visible anchor spheres from the current anchors (slightly larger than placed points, so they're easy to see and grab). */
|
|
62049
62326
|
rebuildGeoMarkers() {
|
|
62050
62327
|
this.setGeoHover(-1);
|
|
62051
62328
|
for (const m of this.activeGeoMarkers) {
|
|
@@ -62056,18 +62333,20 @@ void main() {
|
|
|
62056
62333
|
if (!this.activeGeo)
|
|
62057
62334
|
return;
|
|
62058
62335
|
for (const v of this.activeGeo.getAnchorLocals()) {
|
|
62059
|
-
const marker =
|
|
62336
|
+
const marker = makeAnchorHandle(v, this.o.mesh, this.geodesicColor, this.markerRadius * 1.7);
|
|
62060
62337
|
this.o.scene.add(marker);
|
|
62061
62338
|
this.activeGeoMarkers.push(marker);
|
|
62062
62339
|
}
|
|
62063
62340
|
}
|
|
62064
|
-
/** Redraw the
|
|
62341
|
+
/** Redraw the active geodesic from its current anchors (honoring the closed state); when editing a committed contour, also keep the stored annotation data in sync so export stays correct. */
|
|
62065
62342
|
redrawGeoLine() {
|
|
62066
62343
|
if (!this.activeGeo)
|
|
62067
62344
|
return;
|
|
62068
|
-
const
|
|
62345
|
+
const closed = this.geoClosed;
|
|
62346
|
+
const verts = this.activeGeo.buildVertices(closed);
|
|
62069
62347
|
if (verts.length < 2) {
|
|
62070
|
-
|
|
62348
|
+
// Only dispose the line for an in-progress geodesic; when editing, the line belongs to the annotation.
|
|
62349
|
+
if (this.activeGeoLine && !this.editingId) {
|
|
62071
62350
|
this.o.scene.remove(this.activeGeoLine);
|
|
62072
62351
|
this.disposeObject(this.activeGeoLine);
|
|
62073
62352
|
this.activeGeoLine = undefined;
|
|
@@ -62075,11 +62354,20 @@ void main() {
|
|
|
62075
62354
|
return;
|
|
62076
62355
|
}
|
|
62077
62356
|
if (!this.activeGeoLine) {
|
|
62078
|
-
this.activeGeoLine = makeContourLine(verts, this.geodesicColor,
|
|
62357
|
+
this.activeGeoLine = makeContourLine(verts, this.geodesicColor, closed, this.o.container, this.epsilon, this.o.mesh);
|
|
62079
62358
|
this.o.scene.add(this.activeGeoLine);
|
|
62080
62359
|
}
|
|
62081
62360
|
else {
|
|
62082
|
-
updateContourLine(this.activeGeoLine, verts,
|
|
62361
|
+
updateContourLine(this.activeGeoLine, verts, closed, this.epsilon, this.o.mesh);
|
|
62362
|
+
}
|
|
62363
|
+
// Live-sync the committed annotation's data while it is being edited.
|
|
62364
|
+
if (this.editingId) {
|
|
62365
|
+
const a = this.store.get(this.editingId);
|
|
62366
|
+
if (a) {
|
|
62367
|
+
a.vertices = verts;
|
|
62368
|
+
a.closed = closed;
|
|
62369
|
+
a.anchors = this.activeGeo.getAnchorLocals();
|
|
62370
|
+
}
|
|
62083
62371
|
}
|
|
62084
62372
|
}
|
|
62085
62373
|
/** Discard the in-progress geodesic: remove and dispose the line and all anchors. */
|
|
@@ -62096,6 +62384,7 @@ void main() {
|
|
|
62096
62384
|
}
|
|
62097
62385
|
this.activeGeoMarkers = [];
|
|
62098
62386
|
this.activeGeo = undefined;
|
|
62387
|
+
this.emitInteraction();
|
|
62099
62388
|
}
|
|
62100
62389
|
/** Remove all anchor spheres of the in-progress geodesic (called after committing: only the line remains). */
|
|
62101
62390
|
removeGeoMarkers() {
|
|
@@ -62142,6 +62431,7 @@ void main() {
|
|
|
62142
62431
|
closed,
|
|
62143
62432
|
visible: true,
|
|
62144
62433
|
vertices: verts,
|
|
62434
|
+
anchors: this.activeGeo.getAnchorLocals(),
|
|
62145
62435
|
object3D: this.activeGeoLine,
|
|
62146
62436
|
};
|
|
62147
62437
|
this.store.add(ann);
|
|
@@ -62154,6 +62444,61 @@ void main() {
|
|
|
62154
62444
|
this.removeGeoMarkers();
|
|
62155
62445
|
this.activeGeo = undefined;
|
|
62156
62446
|
this.activeGeoLine = undefined;
|
|
62447
|
+
this.geoClosed = false;
|
|
62448
|
+
this.emitInteraction();
|
|
62449
|
+
}
|
|
62450
|
+
/**
|
|
62451
|
+
* Re-open a committed geodesic contour for editing: rebuild its anchors (from the stored positions)
|
|
62452
|
+
* as draggable markers and reuse its line for live updates. No-op for non-geodesic / anchorless
|
|
62453
|
+
* annotations. The edit session ends on Enter / mode-switch / selecting another / Esc.
|
|
62454
|
+
*/
|
|
62455
|
+
tryOpenGeoEdit(id) {
|
|
62456
|
+
var _a;
|
|
62457
|
+
if (this.editingId === id)
|
|
62458
|
+
return;
|
|
62459
|
+
const a = this.store.get(id);
|
|
62460
|
+
if (!a || a.type !== "contour" || a.mode !== "geodesic" || !a.anchors || a.anchors.length < 2) {
|
|
62461
|
+
return;
|
|
62462
|
+
}
|
|
62463
|
+
// Discard a half-drawn (uncommitted) geodesic before entering the edit session.
|
|
62464
|
+
if (this.activeGeo && !this.editingId)
|
|
62465
|
+
this.clearActiveGeo();
|
|
62466
|
+
const indices = a.anchors.map((v) => this.graph.nearestVertex(new Vector3(v.x, v.y, v.z)));
|
|
62467
|
+
this.activeGeo = GeodesicContour.fromAnchors(this.graph, this.o.mesh, indices);
|
|
62468
|
+
this.activeGeoLine = (_a = a.object3D) !== null && _a !== void 0 ? _a : undefined;
|
|
62469
|
+
this.editingId = id;
|
|
62470
|
+
this.geoClosed = a.closed;
|
|
62471
|
+
this.rebuildGeoMarkers();
|
|
62472
|
+
this.redrawGeoLine();
|
|
62473
|
+
this.emitInteraction();
|
|
62474
|
+
}
|
|
62475
|
+
/**
|
|
62476
|
+
* Finalize an edit session on a committed geodesic: the annotation data was kept in sync live
|
|
62477
|
+
* (redrawGeoLine), so here we just remove the edit markers, notify subscribers, and end the
|
|
62478
|
+
* session — or delete the annotation if it was reduced below 2 anchors.
|
|
62479
|
+
*/
|
|
62480
|
+
recommitGeodesic() {
|
|
62481
|
+
const id = this.editingId;
|
|
62482
|
+
if (!id)
|
|
62483
|
+
return;
|
|
62484
|
+
if (!this.activeGeo || this.activeGeo.anchorCount < 2) {
|
|
62485
|
+
this.removeGeoMarkers();
|
|
62486
|
+
this.activeGeo = undefined;
|
|
62487
|
+
this.activeGeoLine = undefined; // owned by the annotation
|
|
62488
|
+
this.editingId = null;
|
|
62489
|
+
this.geoClosed = false;
|
|
62490
|
+
this.deleteAnnotation(id);
|
|
62491
|
+
this.emitInteraction();
|
|
62492
|
+
return;
|
|
62493
|
+
}
|
|
62494
|
+
this.redrawGeoLine(); // final data sync
|
|
62495
|
+
this.removeGeoMarkers();
|
|
62496
|
+
this.activeGeo = undefined;
|
|
62497
|
+
this.activeGeoLine = undefined; // owned by the annotation, do not dispose
|
|
62498
|
+
this.editingId = null;
|
|
62499
|
+
this.geoClosed = false;
|
|
62500
|
+
this.store.touch();
|
|
62501
|
+
this.emitInteraction();
|
|
62157
62502
|
}
|
|
62158
62503
|
isTypingTarget(e) {
|
|
62159
62504
|
const t = e.target;
|
|
@@ -62163,16 +62508,14 @@ void main() {
|
|
|
62163
62508
|
return tag === "INPUT" || tag === "TEXTAREA" || t.isContentEditable;
|
|
62164
62509
|
}
|
|
62165
62510
|
dispose() {
|
|
62166
|
-
var _a;
|
|
62167
62511
|
this.clearActiveGeo();
|
|
62168
|
-
(_a = this.geoHoverBadge) === null || _a === void 0 ? void 0 : _a.remove();
|
|
62169
|
-
this.geoHoverBadge = undefined;
|
|
62170
62512
|
window.removeEventListener("pointerdown", this.onPointerDown, true);
|
|
62171
62513
|
window.removeEventListener("pointermove", this.onPointerMove, true);
|
|
62172
62514
|
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
62173
62515
|
window.removeEventListener("keydown", this.onKeyDown);
|
|
62174
62516
|
window.removeEventListener("keyup", this.onKeyUp);
|
|
62175
62517
|
window.removeEventListener("resize", this.onResize);
|
|
62518
|
+
window.removeEventListener("contextmenu", this.onContextMenu, true);
|
|
62176
62519
|
}
|
|
62177
62520
|
}
|
|
62178
62521
|
SurfaceAnnotator.TAP_MS = 250;
|
|
@@ -62443,6 +62786,12 @@ void main() {
|
|
|
62443
62786
|
tex.setFrame(0);
|
|
62444
62787
|
if (opts.window)
|
|
62445
62788
|
tex.setWindow(opts.window.center, opts.window.width);
|
|
62789
|
+
// All meshes sit at the origin (world coords live in the geometry), so their
|
|
62790
|
+
// transparent-sort distances tie. three <=0.150 broke the tie by insertion
|
|
62791
|
+
// order; >=0.170 resolves it per-camera, which makes the draw order flip while
|
|
62792
|
+
// rotating (flicker / vanishing surfaces). Pin an explicit, deterministic
|
|
62793
|
+
// order: MRI plane first (backdrop), then the surfaces in definition order.
|
|
62794
|
+
tex.mesh.renderOrder = 0;
|
|
62446
62795
|
const surfaceMeshes = {};
|
|
62447
62796
|
const seqs = [];
|
|
62448
62797
|
surfaceDefs.forEach((def, i) => {
|
|
@@ -62451,6 +62800,7 @@ void main() {
|
|
|
62451
62800
|
const { vtkmaterial } = copperMultipleVtk(def.opts);
|
|
62452
62801
|
const mesh = new Mesh(geometries[0], vtkmaterial);
|
|
62453
62802
|
mesh.name = def.name;
|
|
62803
|
+
mesh.renderOrder = i + 1;
|
|
62454
62804
|
this.scene.add(mesh);
|
|
62455
62805
|
surfaceMeshes[def.name] = mesh;
|
|
62456
62806
|
seqs.push({ mesh, geometries, offset: (_a = def.offset) !== null && _a !== void 0 ? _a : 0 });
|
|
@@ -83598,15 +83948,15 @@ void main() {
|
|
|
83598
83948
|
}
|
|
83599
83949
|
|
|
83600
83950
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
83601
|
-
// "v3.6.
|
|
83951
|
+
// "v3.6.10" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version.
|
|
83602
83952
|
// When copper3d is consumed from local source (no rollup replace step), the identifier is undefined and
|
|
83603
83953
|
// referencing it throws a ReferenceError — guard it so local loading still works.
|
|
83604
83954
|
let _revision = "unknown";
|
|
83605
83955
|
try {
|
|
83606
|
-
_revision = "v3.6.
|
|
83956
|
+
_revision = "v3.6.10";
|
|
83607
83957
|
}
|
|
83608
83958
|
catch (_a) {
|
|
83609
|
-
/* "v3.6.
|
|
83959
|
+
/* "v3.6.10" not injected (local source build) */
|
|
83610
83960
|
}
|
|
83611
83961
|
const REVISION = _revision;
|
|
83612
83962
|
// Expose on global so the version can be read in a production browser console via window.__COPPER3D_VERSION__
|