copper3d 3.6.9 → 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/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 +471 -128
- package/dist/bundle.umd.js +471 -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.esm.js
CHANGED
|
@@ -59646,6 +59646,25 @@ function makePointMarker(v, mesh, color, radius) {
|
|
|
59646
59646
|
m.position.copy(p.addScaledVector(n, radius * 0.5));
|
|
59647
59647
|
m.renderOrder = 999;
|
|
59648
59648
|
return m;
|
|
59649
|
+
}
|
|
59650
|
+
/**
|
|
59651
|
+
* Create a draggable ANCHOR HANDLE (for the geodesic editor): a white outer rim around a
|
|
59652
|
+
* tool-colored core, so the handle stands out against both the (same-colored) contour line and the
|
|
59653
|
+
* grey surface. Returned as a Group; scale it to show hover/selection feedback. depthTest stays ON
|
|
59654
|
+
* (occluded by the model on the far side) and it's lifted a touch more than a plain marker so it
|
|
59655
|
+
* sits above the thickened line rather than sinking into it.
|
|
59656
|
+
*/
|
|
59657
|
+
function makeAnchorHandle(v, mesh, coreColor, radius) {
|
|
59658
|
+
const g = new Group();
|
|
59659
|
+
const rim = new Mesh(new SphereGeometry(radius, 16, 16), new MeshBasicMaterial({ color: "#ffffff" }));
|
|
59660
|
+
rim.renderOrder = 1000;
|
|
59661
|
+
const core = new Mesh(new SphereGeometry(radius * 0.6, 16, 16), new MeshBasicMaterial({ color: coreColor }));
|
|
59662
|
+
core.renderOrder = 1001;
|
|
59663
|
+
g.add(rim);
|
|
59664
|
+
g.add(core);
|
|
59665
|
+
const { p, n } = localVertexToWorld(v, mesh);
|
|
59666
|
+
g.position.copy(p.addScaledVector(n, radius * 0.7));
|
|
59667
|
+
return g;
|
|
59649
59668
|
}
|
|
59650
59669
|
|
|
59651
59670
|
/**
|
|
@@ -61273,6 +61292,60 @@ class GeodesicContour {
|
|
|
61273
61292
|
}
|
|
61274
61293
|
this.anchors.push(v);
|
|
61275
61294
|
}
|
|
61295
|
+
/** Move the anchor at `index` to the vertex nearest `localHitPoint`, rebuilding adjacent segments live. */
|
|
61296
|
+
moveAnchorTo(index, localHitPoint) {
|
|
61297
|
+
if (index < 0 || index >= this.anchors.length)
|
|
61298
|
+
return;
|
|
61299
|
+
this.snapshot();
|
|
61300
|
+
this.anchors[index] = this.graph.nearestVertex(localHitPoint);
|
|
61301
|
+
this.rebuildSegments();
|
|
61302
|
+
}
|
|
61303
|
+
/** Anchor vertex indices (persisted on a committed geodesic so it can be re-opened for editing). */
|
|
61304
|
+
getAnchorIndices() {
|
|
61305
|
+
return this.anchors.slice();
|
|
61306
|
+
}
|
|
61307
|
+
/** Rebuild an editable contour from stored anchor vertex indices (re-opening a committed geodesic). */
|
|
61308
|
+
static fromAnchors(graph, mesh, indices) {
|
|
61309
|
+
const gc = new GeodesicContour(graph, mesh);
|
|
61310
|
+
gc.anchors = indices.slice();
|
|
61311
|
+
gc.rebuildSegments();
|
|
61312
|
+
return gc;
|
|
61313
|
+
}
|
|
61314
|
+
/** Insert a new anchor (snapped to the nearest vertex) at position `index`, rebuilding segments. */
|
|
61315
|
+
insertAnchorAt(index, localHitPoint) {
|
|
61316
|
+
const v = this.graph.nearestVertex(localHitPoint);
|
|
61317
|
+
this.snapshot();
|
|
61318
|
+
const i = Math.max(0, Math.min(index, this.anchors.length));
|
|
61319
|
+
this.anchors.splice(i, 0, v);
|
|
61320
|
+
this.rebuildSegments();
|
|
61321
|
+
}
|
|
61322
|
+
/**
|
|
61323
|
+
* Which anchor interval's surface path passes closest to `worldPoint` — i.e. the edge the user
|
|
61324
|
+
* clicked on, so a new anchor can be inserted there. Returns the interval index (insert after that
|
|
61325
|
+
* anchor, i.e. at index+1), or -1 when there are fewer than 2 anchors or the click is farther than
|
|
61326
|
+
* `maxDist` (world units) from every edge. For a closed contour the trailing closing edge
|
|
61327
|
+
* (last→first) is considered and maps to appending at the end.
|
|
61328
|
+
*/
|
|
61329
|
+
nearestInsertIndex(worldPoint, matrixWorld, closed, maxDist) {
|
|
61330
|
+
if (this.anchors.length < 2)
|
|
61331
|
+
return -1;
|
|
61332
|
+
const segs = this.segments.slice();
|
|
61333
|
+
if (closed && this.anchors.length > 2) {
|
|
61334
|
+
segs.push(this.graph.shortestPath(this.anchors[this.anchors.length - 1], this.anchors[0]));
|
|
61335
|
+
}
|
|
61336
|
+
let best = -1;
|
|
61337
|
+
let bestD = maxDist;
|
|
61338
|
+
for (let si = 0; si < segs.length; si++) {
|
|
61339
|
+
for (const vi of segs[si]) {
|
|
61340
|
+
const d = this.graph.vertexWorld(vi, matrixWorld).distanceTo(worldPoint);
|
|
61341
|
+
if (d < bestD) {
|
|
61342
|
+
bestD = d;
|
|
61343
|
+
best = si;
|
|
61344
|
+
}
|
|
61345
|
+
}
|
|
61346
|
+
}
|
|
61347
|
+
return best;
|
|
61348
|
+
}
|
|
61276
61349
|
/** Remove the anchor at the given index and recompute adjacent segments (allows canceling any middle point). */
|
|
61277
61350
|
removeAnchorAt(index) {
|
|
61278
61351
|
if (index < 0 || index >= this.anchors.length)
|
|
@@ -61324,7 +61397,52 @@ class GeodesicContour {
|
|
|
61324
61397
|
idxPath.push(this.anchors[0]);
|
|
61325
61398
|
}
|
|
61326
61399
|
// The graph geometry is already local space, so emit local vertices directly (world is derived at render time).
|
|
61327
|
-
return
|
|
61400
|
+
return this.smoothPath(idxPath, closed);
|
|
61401
|
+
}
|
|
61402
|
+
/**
|
|
61403
|
+
* Relax the stitched shortest-path polyline so it reads as a smooth curve instead of the raw
|
|
61404
|
+
* voxel staircase, WITHOUT changing the point count (keeps editing/index mapping simple) and
|
|
61405
|
+
* WITHOUT moving the anchors (they stay exact control points). A few Laplacian passes pull each
|
|
61406
|
+
* interior point toward the midpoint of its neighbours; anchors and (for open paths) the two
|
|
61407
|
+
* endpoints are pinned. Normals are carried over from each point's original graph vertex — the
|
|
61408
|
+
* points only shift slightly and the line already floats above the surface by `epsilon`, so the
|
|
61409
|
+
* approximation is invisible and avoids an O(V) nearest-vertex scan per point on every redraw.
|
|
61410
|
+
*/
|
|
61411
|
+
smoothPath(idxPath, closed) {
|
|
61412
|
+
const base = idxPath.map((i) => this.graph.vertexLocal(i));
|
|
61413
|
+
const n = base.length;
|
|
61414
|
+
if (n <= 2)
|
|
61415
|
+
return base;
|
|
61416
|
+
const anchorSet = new Set(this.anchors);
|
|
61417
|
+
const pinned = idxPath.map((i) => anchorSet.has(i));
|
|
61418
|
+
const P = base.map((v) => new Vector3(v.x, v.y, v.z));
|
|
61419
|
+
const ITER = 2;
|
|
61420
|
+
const LAMBDA = 0.5;
|
|
61421
|
+
const mid = new Vector3();
|
|
61422
|
+
for (let it = 0; it < ITER; it++) {
|
|
61423
|
+
const next = P.map((p) => p.clone());
|
|
61424
|
+
for (let k = 0; k < n; k++) {
|
|
61425
|
+
if (pinned[k])
|
|
61426
|
+
continue;
|
|
61427
|
+
if (!closed && (k === 0 || k === n - 1))
|
|
61428
|
+
continue;
|
|
61429
|
+
const a = P[(k - 1 + n) % n];
|
|
61430
|
+
const b = P[(k + 1) % n];
|
|
61431
|
+
mid.addVectors(a, b).multiplyScalar(0.5).sub(P[k]);
|
|
61432
|
+
next[k].copy(P[k]).addScaledVector(mid, LAMBDA);
|
|
61433
|
+
}
|
|
61434
|
+
for (let k = 0; k < n; k++)
|
|
61435
|
+
P[k].copy(next[k]);
|
|
61436
|
+
}
|
|
61437
|
+
return P.map((p, k) => ({
|
|
61438
|
+
x: p.x,
|
|
61439
|
+
y: p.y,
|
|
61440
|
+
z: p.z,
|
|
61441
|
+
nx: base[k].nx,
|
|
61442
|
+
ny: base[k].ny,
|
|
61443
|
+
nz: base[k].nz,
|
|
61444
|
+
faceIndex: -1,
|
|
61445
|
+
}));
|
|
61328
61446
|
}
|
|
61329
61447
|
}
|
|
61330
61448
|
|
|
@@ -61409,6 +61527,10 @@ class AnnotationStore {
|
|
|
61409
61527
|
this.notify();
|
|
61410
61528
|
}
|
|
61411
61529
|
}
|
|
61530
|
+
/** Notify subscribers after an in-place mutation of an existing annotation (e.g. geodesic edit re-commit). */
|
|
61531
|
+
touch() {
|
|
61532
|
+
this.notify();
|
|
61533
|
+
}
|
|
61412
61534
|
toJSON(modelName, mesh, opts = {}) {
|
|
61413
61535
|
var _a;
|
|
61414
61536
|
const space = (_a = opts.space) !== null && _a !== void 0 ? _a : "local";
|
|
@@ -61426,16 +61548,23 @@ class AnnotationStore {
|
|
|
61426
61548
|
model: modelName,
|
|
61427
61549
|
exportedAt: new Date().toISOString(),
|
|
61428
61550
|
space,
|
|
61429
|
-
annotations: this.items.map((a) =>
|
|
61430
|
-
|
|
61431
|
-
|
|
61432
|
-
|
|
61433
|
-
|
|
61434
|
-
|
|
61435
|
-
|
|
61436
|
-
|
|
61437
|
-
|
|
61438
|
-
|
|
61551
|
+
annotations: this.items.map((a) => {
|
|
61552
|
+
const out = {
|
|
61553
|
+
id: a.id,
|
|
61554
|
+
type: a.type,
|
|
61555
|
+
mode: a.mode,
|
|
61556
|
+
label: a.label,
|
|
61557
|
+
color: a.color,
|
|
61558
|
+
closed: a.closed,
|
|
61559
|
+
visible: a.visible,
|
|
61560
|
+
points: a.vertices.map(toPt),
|
|
61561
|
+
};
|
|
61562
|
+
// Geodesic contours also carry their control points so a round-tripped import stays editable.
|
|
61563
|
+
if (a.mode === "geodesic" && a.anchors && a.anchors.length) {
|
|
61564
|
+
out.anchors = a.anchors.map(toPt);
|
|
61565
|
+
}
|
|
61566
|
+
return out;
|
|
61567
|
+
}),
|
|
61439
61568
|
};
|
|
61440
61569
|
}
|
|
61441
61570
|
}
|
|
@@ -61462,11 +61591,20 @@ class SurfaceAnnotator {
|
|
|
61462
61591
|
this.managed = new Set();
|
|
61463
61592
|
this.seq = 0;
|
|
61464
61593
|
this.selectedId = null;
|
|
61465
|
-
this.activeGeoMarkers = []; // visible
|
|
61594
|
+
this.activeGeoMarkers = []; // visible anchor handles of the active geodesic
|
|
61466
61595
|
this.geoRay = new Raycaster();
|
|
61467
61596
|
this.geoNdc = new Vector2();
|
|
61468
61597
|
this.hoveredGeoMarker = -1;
|
|
61598
|
+
this.draggingAnchor = -1; // anchor index currently being dragged (-1 = none)
|
|
61599
|
+
this.geoRedrawScheduled = false; // rAF coalescing flag for live drag redraw
|
|
61600
|
+
this.editingId = null; // committed geodesic re-opened for editing (Task 1.5)
|
|
61601
|
+
this.geoClosed = false; // whether the active/edited geodesic is a closed loop
|
|
61469
61602
|
this._projV = new Vector3();
|
|
61603
|
+
/** Block the OS right-click menu when the pointer is over the WebGL canvas (not the HTML panels). */
|
|
61604
|
+
this.onContextMenu = (e) => {
|
|
61605
|
+
if (this.insideContainer(e))
|
|
61606
|
+
e.preventDefault();
|
|
61607
|
+
};
|
|
61470
61608
|
/** Update the pixel resolution of all fat lines on window resize, otherwise line width distorts. */
|
|
61471
61609
|
this.onResize = () => {
|
|
61472
61610
|
const w = this.o.container.clientWidth;
|
|
@@ -61483,10 +61621,50 @@ class SurfaceAnnotator {
|
|
|
61483
61621
|
(this.activeGeoLine.material.resolution.set(w, h));
|
|
61484
61622
|
};
|
|
61485
61623
|
this.onPointerDown = (e) => {
|
|
61486
|
-
if (e.button !== 0)
|
|
61487
|
-
return; // left button only
|
|
61488
61624
|
if (!this.insideContainer(e))
|
|
61489
61625
|
return;
|
|
61626
|
+
// Geodesic anchor editing (drag to move / right-click to delete) is available whenever an
|
|
61627
|
+
// editable geodesic exists — even outside draw-lock — because it requires precisely hitting an
|
|
61628
|
+
// existing anchor. This lets the user rotate the model freely and still grab a point. Handled
|
|
61629
|
+
// BEFORE the button/drawing gates below.
|
|
61630
|
+
if (this.armed === "geodesic" && this.activeGeo) {
|
|
61631
|
+
if (e.button === 2) {
|
|
61632
|
+
const pick = this.pickGeoMarker(e);
|
|
61633
|
+
if (pick >= 0) {
|
|
61634
|
+
e.preventDefault();
|
|
61635
|
+
this.activeGeo.removeAnchorAt(pick);
|
|
61636
|
+
this.afterGeoEdit();
|
|
61637
|
+
return;
|
|
61638
|
+
}
|
|
61639
|
+
}
|
|
61640
|
+
else if (e.button === 0) {
|
|
61641
|
+
const pick = this.pickGeoMarker(e);
|
|
61642
|
+
if (pick >= 0) {
|
|
61643
|
+
e.preventDefault();
|
|
61644
|
+
this.draggingAnchor = pick;
|
|
61645
|
+
this.pointerDown = true;
|
|
61646
|
+
this.o.controls.enabled = false; // suppress camera while dragging the anchor
|
|
61647
|
+
this.o.container.style.cursor = "grabbing";
|
|
61648
|
+
return;
|
|
61649
|
+
}
|
|
61650
|
+
// Missed every anchor → if the click lands on the line body, insert a new anchor there
|
|
61651
|
+
// (works in an edit session or an in-progress geodesic, regardless of draw-lock).
|
|
61652
|
+
if (this.activeGeo.anchorCount >= 2) {
|
|
61653
|
+
const h = this.hit(e);
|
|
61654
|
+
if (h) {
|
|
61655
|
+
const insertAt = this.activeGeo.nearestInsertIndex(h.point, this.o.mesh.matrixWorld, this.geoClosed, this.minGap * 4);
|
|
61656
|
+
if (insertAt >= 0) {
|
|
61657
|
+
e.preventDefault();
|
|
61658
|
+
this.activeGeo.insertAnchorAt(insertAt + 1, this.o.mesh.worldToLocal(h.point.clone()));
|
|
61659
|
+
this.afterGeoEdit();
|
|
61660
|
+
return;
|
|
61661
|
+
}
|
|
61662
|
+
}
|
|
61663
|
+
}
|
|
61664
|
+
}
|
|
61665
|
+
}
|
|
61666
|
+
if (e.button !== 0)
|
|
61667
|
+
return; // the remaining tools are left-button only
|
|
61490
61668
|
this.pointerDown = true;
|
|
61491
61669
|
// Only act with the armed tool when in drawing mode (drawLock or spaceHeld).
|
|
61492
61670
|
// When not drawing, navigation is the default — pointer events go to camera controls.
|
|
@@ -61526,19 +61704,8 @@ class SurfaceAnnotator {
|
|
|
61526
61704
|
return;
|
|
61527
61705
|
}
|
|
61528
61706
|
if (activeTool === "geodesic") {
|
|
61529
|
-
//
|
|
61530
|
-
|
|
61531
|
-
if (pick >= 0 && this.activeGeo) {
|
|
61532
|
-
this.activeGeo.removeAnchorAt(pick);
|
|
61533
|
-
if (this.activeGeo.anchorCount === 0)
|
|
61534
|
-
this.clearActiveGeo();
|
|
61535
|
-
else {
|
|
61536
|
-
this.rebuildGeoMarkers();
|
|
61537
|
-
this.redrawGeoLine();
|
|
61538
|
-
}
|
|
61539
|
-
return;
|
|
61540
|
-
}
|
|
61541
|
-
// Otherwise drop a new anchor on the surface.
|
|
61707
|
+
// Anchor grab/delete was already handled above; here we only drop a NEW anchor on the surface
|
|
61708
|
+
// (clicks on an existing anchor never reach this point).
|
|
61542
61709
|
const h = this.hit(e);
|
|
61543
61710
|
if (!h)
|
|
61544
61711
|
return;
|
|
@@ -61547,11 +61714,20 @@ class SurfaceAnnotator {
|
|
|
61547
61714
|
}
|
|
61548
61715
|
const local = this.o.mesh.worldToLocal(h.point.clone());
|
|
61549
61716
|
this.activeGeo.addAnchor(local);
|
|
61550
|
-
this.
|
|
61551
|
-
this.redrawGeoLine();
|
|
61717
|
+
this.afterGeoEdit();
|
|
61552
61718
|
}
|
|
61553
61719
|
};
|
|
61554
61720
|
this.onPointerMove = (e) => {
|
|
61721
|
+
// Live anchor drag: move the grabbed anchor to the surface point under the cursor and redraw
|
|
61722
|
+
// (both adjacent segments recompute inside moveAnchorTo). Takes priority over everything else.
|
|
61723
|
+
if (this.draggingAnchor >= 0) {
|
|
61724
|
+
const h = this.hit(e);
|
|
61725
|
+
if (h && this.activeGeo) {
|
|
61726
|
+
this.activeGeo.moveAnchorTo(this.draggingAnchor, this.o.mesh.worldToLocal(h.point.clone()));
|
|
61727
|
+
this.scheduleGeoRedraw();
|
|
61728
|
+
}
|
|
61729
|
+
return;
|
|
61730
|
+
}
|
|
61555
61731
|
// Track if user dragged while space was held (so a hold-drag is not treated as a tap).
|
|
61556
61732
|
if (this.spaceHeld && this.pointerDown) {
|
|
61557
61733
|
this.spaceDragged = true;
|
|
@@ -61583,6 +61759,15 @@ class SurfaceAnnotator {
|
|
|
61583
61759
|
}
|
|
61584
61760
|
};
|
|
61585
61761
|
this.onPointerUp = () => {
|
|
61762
|
+
// Finish an anchor drag: restore camera gating + cursor. (Re-commit of an edited committed
|
|
61763
|
+
// contour is wired in Task 1.5.)
|
|
61764
|
+
if (this.draggingAnchor >= 0) {
|
|
61765
|
+
this.draggingAnchor = -1;
|
|
61766
|
+
this.pointerDown = false;
|
|
61767
|
+
this.o.container.style.cursor = this.hoveredGeoMarker >= 0 ? "grab" : "";
|
|
61768
|
+
this.applyCameraGating();
|
|
61769
|
+
return;
|
|
61770
|
+
}
|
|
61586
61771
|
if (!this.pointerDown)
|
|
61587
61772
|
return;
|
|
61588
61773
|
this.pointerDown = false;
|
|
@@ -61624,6 +61809,18 @@ class SurfaceAnnotator {
|
|
|
61624
61809
|
return;
|
|
61625
61810
|
}
|
|
61626
61811
|
if (e.key === "Escape") {
|
|
61812
|
+
// First Esc clears an active selection / in-progress geodesic while staying in the current
|
|
61813
|
+
// tool; a second Esc (nothing selected) returns to Navigate.
|
|
61814
|
+
if (this.selectedId || this.activeGeo) {
|
|
61815
|
+
if (this.editingId)
|
|
61816
|
+
this.recommitGeodesic();
|
|
61817
|
+
else if (this.activeGeo)
|
|
61818
|
+
this.clearActiveGeo();
|
|
61819
|
+
this.setSelected(null);
|
|
61820
|
+
this.drawLock = false;
|
|
61821
|
+
this.applyCameraGating();
|
|
61822
|
+
return;
|
|
61823
|
+
}
|
|
61627
61824
|
this.drawLock = false;
|
|
61628
61825
|
this.setMode("navigate");
|
|
61629
61826
|
return;
|
|
@@ -61639,8 +61836,11 @@ class SurfaceAnnotator {
|
|
|
61639
61836
|
return;
|
|
61640
61837
|
}
|
|
61641
61838
|
if (e.key === "Enter") {
|
|
61642
|
-
//
|
|
61643
|
-
|
|
61839
|
+
// Editing a committed geodesic → finalize the edit; a fresh geodesic in progress → finish and
|
|
61840
|
+
// close it; otherwise close the most recent freehand line.
|
|
61841
|
+
if (this.editingId)
|
|
61842
|
+
this.recommitGeodesic();
|
|
61843
|
+
else if (this.activeGeo)
|
|
61644
61844
|
this.finishGeodesic();
|
|
61645
61845
|
else
|
|
61646
61846
|
this.closeLastContour();
|
|
@@ -61693,6 +61893,10 @@ class SurfaceAnnotator {
|
|
|
61693
61893
|
window.addEventListener("keydown", this.onKeyDown);
|
|
61694
61894
|
window.addEventListener("keyup", this.onKeyUp);
|
|
61695
61895
|
window.addEventListener("resize", this.onResize);
|
|
61896
|
+
// Suppress the browser's native context menu over the WebGL canvas in EVERY mode: right-click
|
|
61897
|
+
// there is used for camera pan (and, in geodesic mode, delete-anchor), so the OS menu must
|
|
61898
|
+
// never appear. Guarded by insideContainer → right-clicking the HTML panels keeps its menu.
|
|
61899
|
+
window.addEventListener("contextmenu", this.onContextMenu, true);
|
|
61696
61900
|
this.applyCameraGating();
|
|
61697
61901
|
}
|
|
61698
61902
|
get freehandColor() {
|
|
@@ -61713,14 +61917,22 @@ class SurfaceAnnotator {
|
|
|
61713
61917
|
}
|
|
61714
61918
|
setMode(m) {
|
|
61715
61919
|
var _a, _b;
|
|
61716
|
-
// When leaving geodesic mode
|
|
61717
|
-
|
|
61718
|
-
|
|
61920
|
+
// When leaving geodesic mode: commit an in-progress EDIT of a committed contour, or discard a
|
|
61921
|
+
// not-yet-committed new geodesic (clear leftover anchors and line).
|
|
61922
|
+
if (m !== "geodesic") {
|
|
61923
|
+
if (this.editingId)
|
|
61924
|
+
this.recommitGeodesic();
|
|
61925
|
+
else if (this.activeGeo)
|
|
61926
|
+
this.clearActiveGeo();
|
|
61927
|
+
}
|
|
61719
61928
|
this.mode = m;
|
|
61720
61929
|
// Record armed tool when choosing a drawing mode (not navigate).
|
|
61721
61930
|
// Camera gating is NOT changed here — the user must use Space to enter drawing.
|
|
61722
61931
|
if (m !== "navigate")
|
|
61723
61932
|
this.armed = m;
|
|
61933
|
+
// Returning to Navigate clears the selection too (put everything away).
|
|
61934
|
+
if (m === "navigate")
|
|
61935
|
+
this.setSelected(null);
|
|
61724
61936
|
this.applyCameraGating();
|
|
61725
61937
|
(_b = (_a = this.o).onModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, m);
|
|
61726
61938
|
}
|
|
@@ -61732,21 +61944,23 @@ class SurfaceAnnotator {
|
|
|
61732
61944
|
return this.store.list();
|
|
61733
61945
|
}
|
|
61734
61946
|
undo() {
|
|
61735
|
-
//
|
|
61736
|
-
//
|
|
61947
|
+
// A geodesic being drawn/edited → undo the most recent anchor edit. afterGeoEdit() handles both
|
|
61948
|
+
// the in-progress and committed-edit cases uniformly, including degenerate cleanup (so undo can't
|
|
61949
|
+
// dispose a committed line or leave a stale edit session).
|
|
61737
61950
|
if (this.activeGeo && this.activeGeo.canUndo()) {
|
|
61738
61951
|
this.activeGeo.undoEdit();
|
|
61739
|
-
|
|
61740
|
-
this.clearActiveGeo();
|
|
61741
|
-
else {
|
|
61742
|
-
this.rebuildGeoMarkers();
|
|
61743
|
-
this.redrawGeoLine();
|
|
61744
|
-
}
|
|
61952
|
+
this.afterGeoEdit();
|
|
61745
61953
|
return;
|
|
61746
61954
|
}
|
|
61747
61955
|
this.store.undo();
|
|
61748
61956
|
}
|
|
61749
61957
|
clearAll() {
|
|
61958
|
+
// Drop any edit session first; the edited line is owned by the store and disposed by clear() below.
|
|
61959
|
+
if (this.editingId) {
|
|
61960
|
+
this.activeGeoLine = undefined;
|
|
61961
|
+
this.editingId = null;
|
|
61962
|
+
this.geoClosed = false;
|
|
61963
|
+
}
|
|
61750
61964
|
const removed = this.store.clear();
|
|
61751
61965
|
removed.forEach((a) => {
|
|
61752
61966
|
if (a.object3D) {
|
|
@@ -61759,13 +61973,49 @@ class SurfaceAnnotator {
|
|
|
61759
61973
|
this.clearActiveGeo();
|
|
61760
61974
|
}
|
|
61761
61975
|
deleteAnnotation(id) {
|
|
61976
|
+
// If we're deleting the geodesic currently being edited, tear the edit session down first so
|
|
61977
|
+
// its anchor handles aren't orphaned in the scene (the line itself is removed by reconcile).
|
|
61978
|
+
if (this.editingId === id) {
|
|
61979
|
+
this.removeGeoMarkers();
|
|
61980
|
+
this.activeGeo = undefined;
|
|
61981
|
+
this.activeGeoLine = undefined;
|
|
61982
|
+
this.editingId = null;
|
|
61983
|
+
this.geoClosed = false;
|
|
61984
|
+
}
|
|
61762
61985
|
if (this.selectedId === id)
|
|
61763
|
-
this.
|
|
61986
|
+
this.setSelected(null);
|
|
61764
61987
|
this.store.remove(id);
|
|
61988
|
+
this.emitInteraction();
|
|
61765
61989
|
}
|
|
61766
61990
|
selectAnnotation(id) {
|
|
61991
|
+
var _a;
|
|
61992
|
+
// Commit any open geodesic edit before the selection changes.
|
|
61993
|
+
if (this.editingId && this.editingId !== id)
|
|
61994
|
+
this.recommitGeodesic();
|
|
61767
61995
|
this.selectedId = id;
|
|
61768
61996
|
this.applySelection();
|
|
61997
|
+
if (!id)
|
|
61998
|
+
return;
|
|
61999
|
+
const a = this.store.get(id);
|
|
62000
|
+
if (!a)
|
|
62001
|
+
return;
|
|
62002
|
+
// Selecting an annotation switches to the tool that drew it, so you can carry on editing it
|
|
62003
|
+
// (and a geodesic opens its anchor handles). points → Place point; contour → its draw mode.
|
|
62004
|
+
const tool = a.type === "points" ? "point" : (_a = a.mode) !== null && _a !== void 0 ? _a : "freehand";
|
|
62005
|
+
if (this.mode !== tool)
|
|
62006
|
+
this.setMode(tool);
|
|
62007
|
+
// With Geodesic now active, re-open the contour's anchors for editing.
|
|
62008
|
+
if (this.mode === "geodesic")
|
|
62009
|
+
this.tryOpenGeoEdit(id);
|
|
62010
|
+
}
|
|
62011
|
+
/** Change the selection from inside the engine (delete / deselect-on-navigate) and mirror it to the UI. */
|
|
62012
|
+
setSelected(id) {
|
|
62013
|
+
var _a, _b;
|
|
62014
|
+
if (this.selectedId === id)
|
|
62015
|
+
return;
|
|
62016
|
+
this.selectedId = id;
|
|
62017
|
+
this.applySelection();
|
|
62018
|
+
(_b = (_a = this.o).onSelectionChange) === null || _b === void 0 ? void 0 : _b.call(_a, id);
|
|
61769
62019
|
}
|
|
61770
62020
|
/** Redraw the corresponding three object after a color change. */
|
|
61771
62021
|
refreshAnnotation(id) {
|
|
@@ -61796,17 +62046,11 @@ class SurfaceAnnotator {
|
|
|
61796
62046
|
let count = 0;
|
|
61797
62047
|
let maxImported = this.seq;
|
|
61798
62048
|
for (const a of (_a = payload.annotations) !== null && _a !== void 0 ? _a : []) {
|
|
61799
|
-
const verts = a.points.map((p) =>
|
|
61800
|
-
|
|
61801
|
-
|
|
61802
|
-
|
|
61803
|
-
|
|
61804
|
-
nx = nrm.x;
|
|
61805
|
-
ny = nrm.y;
|
|
61806
|
-
nz = nrm.z;
|
|
61807
|
-
}
|
|
61808
|
-
return { x, y, z, nx, ny, nz, faceIndex: 0 };
|
|
61809
|
-
});
|
|
62049
|
+
const verts = a.points.map((p) => this.rawToVertex(p));
|
|
62050
|
+
// Geodesic control points, if the payload carries them → keep the contour editable.
|
|
62051
|
+
const anchors = a.mode === "geodesic" && a.anchors && a.anchors.length
|
|
62052
|
+
? a.anchors.map((p) => this.rawToVertex(p))
|
|
62053
|
+
: undefined;
|
|
61810
62054
|
if (a.type === "points") {
|
|
61811
62055
|
for (const v of verts) {
|
|
61812
62056
|
// Only reuse the provided id for a single-point entry (the round-trip case);
|
|
@@ -61847,6 +62091,7 @@ class SurfaceAnnotator {
|
|
|
61847
62091
|
closed: a.closed,
|
|
61848
62092
|
visible: (_d = a.visible) !== null && _d !== void 0 ? _d : true,
|
|
61849
62093
|
vertices: verts,
|
|
62094
|
+
anchors,
|
|
61850
62095
|
object3D: line,
|
|
61851
62096
|
});
|
|
61852
62097
|
const m = id.match(/^a(\d+)$/);
|
|
@@ -61865,10 +62110,19 @@ class SurfaceAnnotator {
|
|
|
61865
62110
|
return this.drawLock || this.spaceHeld;
|
|
61866
62111
|
}
|
|
61867
62112
|
applyCameraGating() {
|
|
61868
|
-
var _a, _b;
|
|
61869
62113
|
// Default is navigate (camera enabled). Drawing only when drawLock or spaceHeld.
|
|
61870
62114
|
this.o.controls.enabled = !this.drawing;
|
|
61871
|
-
|
|
62115
|
+
this.emitInteraction();
|
|
62116
|
+
}
|
|
62117
|
+
/** Emit the current interaction state (camera gating + whether a geodesic is editable). */
|
|
62118
|
+
emitInteraction() {
|
|
62119
|
+
var _a, _b;
|
|
62120
|
+
(_b = (_a = this.o).onInteractionChange) === null || _b === void 0 ? void 0 : _b.call(_a, {
|
|
62121
|
+
drawing: this.drawing,
|
|
62122
|
+
armed: this.armed,
|
|
62123
|
+
locked: this.drawLock,
|
|
62124
|
+
editing: !!this.activeGeo,
|
|
62125
|
+
});
|
|
61872
62126
|
}
|
|
61873
62127
|
/** Reconcile the scene against store.list(): add missing objects, remove deleted ones (no dispose, kept for undo restore). */
|
|
61874
62128
|
reconcile() {
|
|
@@ -61911,18 +62165,33 @@ class SurfaceAnnotator {
|
|
|
61911
62165
|
}
|
|
61912
62166
|
}
|
|
61913
62167
|
disposeObject(o) {
|
|
61914
|
-
|
|
61915
|
-
|
|
61916
|
-
|
|
61917
|
-
|
|
61918
|
-
|
|
61919
|
-
mat
|
|
61920
|
-
|
|
61921
|
-
|
|
62168
|
+
// Traverse so composite handles (Group of rim + core meshes) are fully freed, not just the root.
|
|
62169
|
+
o.traverse((c) => {
|
|
62170
|
+
var _a, _b, _c;
|
|
62171
|
+
const any = c;
|
|
62172
|
+
(_b = (_a = any.geometry) === null || _a === void 0 ? void 0 : _a.dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
62173
|
+
const mat = any.material;
|
|
62174
|
+
if (Array.isArray(mat))
|
|
62175
|
+
mat.forEach((m) => m.dispose());
|
|
62176
|
+
else
|
|
62177
|
+
(_c = mat === null || mat === void 0 ? void 0 : mat.dispose) === null || _c === void 0 ? void 0 : _c.call(mat);
|
|
62178
|
+
});
|
|
61922
62179
|
}
|
|
61923
62180
|
nextId() {
|
|
61924
62181
|
return "a" + ++this.seq;
|
|
61925
62182
|
}
|
|
62183
|
+
/** 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. */
|
|
62184
|
+
rawToVertex(p) {
|
|
62185
|
+
const [x, y, z] = p;
|
|
62186
|
+
let nx = p[3], ny = p[4], nz = p[5];
|
|
62187
|
+
if (nx === undefined || ny === undefined || nz === undefined) {
|
|
62188
|
+
const nrm = this.graph.nearestNormalLocal(new Vector3(x, y, z));
|
|
62189
|
+
nx = nrm.x;
|
|
62190
|
+
ny = nrm.y;
|
|
62191
|
+
nz = nrm.z;
|
|
62192
|
+
}
|
|
62193
|
+
return { x, y, z, nx, ny, nz, faceIndex: 0 };
|
|
62194
|
+
}
|
|
61926
62195
|
hit(e) {
|
|
61927
62196
|
return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
|
|
61928
62197
|
}
|
|
@@ -61937,6 +62206,55 @@ class SurfaceAnnotator {
|
|
|
61937
62206
|
const t = e.target;
|
|
61938
62207
|
return !!t && t.tagName === "CANVAS" && this.o.container.contains(t);
|
|
61939
62208
|
}
|
|
62209
|
+
/** After any anchor edit: refresh markers + line; drop/delete the contour when it becomes degenerate. */
|
|
62210
|
+
afterGeoEdit() {
|
|
62211
|
+
if (!this.activeGeo)
|
|
62212
|
+
return;
|
|
62213
|
+
if (this.editingId) {
|
|
62214
|
+
// Editing a committed contour: fewer than 2 anchors → delete the whole annotation.
|
|
62215
|
+
if (this.activeGeo.anchorCount < 2) {
|
|
62216
|
+
const id = this.editingId;
|
|
62217
|
+
this.removeGeoMarkers();
|
|
62218
|
+
this.activeGeo = undefined;
|
|
62219
|
+
this.activeGeoLine = undefined; // owned by the annotation; deleteAnnotation disposes it
|
|
62220
|
+
this.editingId = null;
|
|
62221
|
+
this.geoClosed = false;
|
|
62222
|
+
this.deleteAnnotation(id);
|
|
62223
|
+
this.emitInteraction();
|
|
62224
|
+
return;
|
|
62225
|
+
}
|
|
62226
|
+
this.rebuildGeoMarkers();
|
|
62227
|
+
this.redrawGeoLine();
|
|
62228
|
+
this.emitInteraction();
|
|
62229
|
+
return;
|
|
62230
|
+
}
|
|
62231
|
+
// In-progress new geodesic.
|
|
62232
|
+
if (this.activeGeo.anchorCount === 0) {
|
|
62233
|
+
this.clearActiveGeo();
|
|
62234
|
+
}
|
|
62235
|
+
else {
|
|
62236
|
+
this.rebuildGeoMarkers();
|
|
62237
|
+
this.redrawGeoLine();
|
|
62238
|
+
}
|
|
62239
|
+
this.emitInteraction();
|
|
62240
|
+
}
|
|
62241
|
+
/** Coalesce live drag redraws to one per animation frame (moveAnchorTo runs Dijkstra per move). */
|
|
62242
|
+
scheduleGeoRedraw() {
|
|
62243
|
+
if (this.geoRedrawScheduled)
|
|
62244
|
+
return;
|
|
62245
|
+
this.geoRedrawScheduled = true;
|
|
62246
|
+
requestAnimationFrame(() => {
|
|
62247
|
+
var _a;
|
|
62248
|
+
this.geoRedrawScheduled = false;
|
|
62249
|
+
this.rebuildGeoMarkers();
|
|
62250
|
+
this.redrawGeoLine();
|
|
62251
|
+
if (this.draggingAnchor >= 0) {
|
|
62252
|
+
this.o.container.style.cursor = "grabbing";
|
|
62253
|
+
// keep the grabbed handle emphasized through the live rebuilds
|
|
62254
|
+
(_a = this.activeGeoMarkers[this.draggingAnchor]) === null || _a === void 0 ? void 0 : _a.scale.setScalar(1.4);
|
|
62255
|
+
}
|
|
62256
|
+
});
|
|
62257
|
+
}
|
|
61940
62258
|
/**
|
|
61941
62259
|
* Pick an in-progress anchor: return the index of the nearest anchor (screen pixel distance <
|
|
61942
62260
|
* tolerance), or -1 on a miss.
|
|
@@ -61973,71 +62291,30 @@ class SurfaceAnnotator {
|
|
|
61973
62291
|
}
|
|
61974
62292
|
return best;
|
|
61975
62293
|
}
|
|
61976
|
-
/**
|
|
62294
|
+
/**
|
|
62295
|
+
* Set the currently hovered anchor (-1 = none): scale it up as a highlight and show a "grab"
|
|
62296
|
+
* cursor to signal it's draggable. Delete is now a right-click (no floating ✕ affordance).
|
|
62297
|
+
*/
|
|
61977
62298
|
setGeoHover(idx) {
|
|
61978
|
-
if (idx === this.hoveredGeoMarker)
|
|
61979
|
-
if (idx >= 0)
|
|
61980
|
-
this.positionGeoBadge(this.activeGeoMarkers[idx]);
|
|
62299
|
+
if (idx === this.hoveredGeoMarker)
|
|
61981
62300
|
return;
|
|
61982
|
-
}
|
|
61983
62301
|
const prev = this.activeGeoMarkers[this.hoveredGeoMarker];
|
|
61984
62302
|
if (prev)
|
|
61985
62303
|
prev.scale.setScalar(1);
|
|
61986
62304
|
this.hoveredGeoMarker = idx;
|
|
61987
62305
|
const cur = this.activeGeoMarkers[idx];
|
|
62306
|
+
// Don't fight the "grabbing" cursor set during an active drag.
|
|
62307
|
+
if (this.draggingAnchor >= 0)
|
|
62308
|
+
return;
|
|
61988
62309
|
if (cur) {
|
|
61989
|
-
cur.scale.setScalar(1.
|
|
61990
|
-
this.
|
|
61991
|
-
this.positionGeoBadge(cur);
|
|
61992
|
-
this.o.container.style.cursor = "pointer";
|
|
62310
|
+
cur.scale.setScalar(1.35);
|
|
62311
|
+
this.o.container.style.cursor = "grab";
|
|
61993
62312
|
}
|
|
61994
62313
|
else {
|
|
61995
|
-
if (this.geoHoverBadge)
|
|
61996
|
-
this.geoHoverBadge.style.display = "none";
|
|
61997
62314
|
this.o.container.style.cursor = "";
|
|
61998
62315
|
}
|
|
61999
62316
|
}
|
|
62000
|
-
/**
|
|
62001
|
-
ensureGeoBadge() {
|
|
62002
|
-
if (this.geoHoverBadge)
|
|
62003
|
-
return this.geoHoverBadge;
|
|
62004
|
-
const b = document.createElement("div");
|
|
62005
|
-
b.textContent = "✕";
|
|
62006
|
-
Object.assign(b.style, {
|
|
62007
|
-
position: "absolute",
|
|
62008
|
-
width: "16px",
|
|
62009
|
-
height: "16px",
|
|
62010
|
-
borderRadius: "50%",
|
|
62011
|
-
background: "#ff5d6c",
|
|
62012
|
-
color: "#fff",
|
|
62013
|
-
font: "700 10px/1 system-ui, sans-serif",
|
|
62014
|
-
display: "none",
|
|
62015
|
-
alignItems: "center",
|
|
62016
|
-
justifyContent: "center",
|
|
62017
|
-
pointerEvents: "none",
|
|
62018
|
-
zIndex: "15",
|
|
62019
|
-
boxShadow: "0 2px 6px rgba(0,0,0,.45)",
|
|
62020
|
-
transform: "translate(-50%, -50%)",
|
|
62021
|
-
left: "0px",
|
|
62022
|
-
top: "0px",
|
|
62023
|
-
});
|
|
62024
|
-
this.o.container.appendChild(b);
|
|
62025
|
-
this.geoHoverBadge = b;
|
|
62026
|
-
return b;
|
|
62027
|
-
}
|
|
62028
|
-
/** Position the ✕ badge at the anchor's screen projection (dead center, i.e. where the cursor hovers, to avoid ambiguity). */
|
|
62029
|
-
positionGeoBadge(marker) {
|
|
62030
|
-
if (!marker)
|
|
62031
|
-
return;
|
|
62032
|
-
const b = this.ensureGeoBadge();
|
|
62033
|
-
this._projV.copy(marker.position).project(this.o.camera);
|
|
62034
|
-
const rect = this.o.container.getBoundingClientRect();
|
|
62035
|
-
const x = (this._projV.x * 0.5 + 0.5) * rect.width;
|
|
62036
|
-
const y = (-this._projV.y * 0.5 + 0.5) * rect.height;
|
|
62037
|
-
b.style.left = `${x}px`;
|
|
62038
|
-
b.style.top = `${y}px`;
|
|
62039
|
-
}
|
|
62040
|
-
/** Rebuild the visible anchor spheres from the current anchors (slightly larger than placed points, so they're easy to see and click to cancel). */
|
|
62317
|
+
/** Rebuild the visible anchor spheres from the current anchors (slightly larger than placed points, so they're easy to see and grab). */
|
|
62041
62318
|
rebuildGeoMarkers() {
|
|
62042
62319
|
this.setGeoHover(-1);
|
|
62043
62320
|
for (const m of this.activeGeoMarkers) {
|
|
@@ -62048,18 +62325,20 @@ class SurfaceAnnotator {
|
|
|
62048
62325
|
if (!this.activeGeo)
|
|
62049
62326
|
return;
|
|
62050
62327
|
for (const v of this.activeGeo.getAnchorLocals()) {
|
|
62051
|
-
const marker =
|
|
62328
|
+
const marker = makeAnchorHandle(v, this.o.mesh, this.geodesicColor, this.markerRadius * 1.7);
|
|
62052
62329
|
this.o.scene.add(marker);
|
|
62053
62330
|
this.activeGeoMarkers.push(marker);
|
|
62054
62331
|
}
|
|
62055
62332
|
}
|
|
62056
|
-
/** Redraw the
|
|
62333
|
+
/** 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. */
|
|
62057
62334
|
redrawGeoLine() {
|
|
62058
62335
|
if (!this.activeGeo)
|
|
62059
62336
|
return;
|
|
62060
|
-
const
|
|
62337
|
+
const closed = this.geoClosed;
|
|
62338
|
+
const verts = this.activeGeo.buildVertices(closed);
|
|
62061
62339
|
if (verts.length < 2) {
|
|
62062
|
-
|
|
62340
|
+
// Only dispose the line for an in-progress geodesic; when editing, the line belongs to the annotation.
|
|
62341
|
+
if (this.activeGeoLine && !this.editingId) {
|
|
62063
62342
|
this.o.scene.remove(this.activeGeoLine);
|
|
62064
62343
|
this.disposeObject(this.activeGeoLine);
|
|
62065
62344
|
this.activeGeoLine = undefined;
|
|
@@ -62067,11 +62346,20 @@ class SurfaceAnnotator {
|
|
|
62067
62346
|
return;
|
|
62068
62347
|
}
|
|
62069
62348
|
if (!this.activeGeoLine) {
|
|
62070
|
-
this.activeGeoLine = makeContourLine(verts, this.geodesicColor,
|
|
62349
|
+
this.activeGeoLine = makeContourLine(verts, this.geodesicColor, closed, this.o.container, this.epsilon, this.o.mesh);
|
|
62071
62350
|
this.o.scene.add(this.activeGeoLine);
|
|
62072
62351
|
}
|
|
62073
62352
|
else {
|
|
62074
|
-
updateContourLine(this.activeGeoLine, verts,
|
|
62353
|
+
updateContourLine(this.activeGeoLine, verts, closed, this.epsilon, this.o.mesh);
|
|
62354
|
+
}
|
|
62355
|
+
// Live-sync the committed annotation's data while it is being edited.
|
|
62356
|
+
if (this.editingId) {
|
|
62357
|
+
const a = this.store.get(this.editingId);
|
|
62358
|
+
if (a) {
|
|
62359
|
+
a.vertices = verts;
|
|
62360
|
+
a.closed = closed;
|
|
62361
|
+
a.anchors = this.activeGeo.getAnchorLocals();
|
|
62362
|
+
}
|
|
62075
62363
|
}
|
|
62076
62364
|
}
|
|
62077
62365
|
/** Discard the in-progress geodesic: remove and dispose the line and all anchors. */
|
|
@@ -62088,6 +62376,7 @@ class SurfaceAnnotator {
|
|
|
62088
62376
|
}
|
|
62089
62377
|
this.activeGeoMarkers = [];
|
|
62090
62378
|
this.activeGeo = undefined;
|
|
62379
|
+
this.emitInteraction();
|
|
62091
62380
|
}
|
|
62092
62381
|
/** Remove all anchor spheres of the in-progress geodesic (called after committing: only the line remains). */
|
|
62093
62382
|
removeGeoMarkers() {
|
|
@@ -62134,6 +62423,7 @@ class SurfaceAnnotator {
|
|
|
62134
62423
|
closed,
|
|
62135
62424
|
visible: true,
|
|
62136
62425
|
vertices: verts,
|
|
62426
|
+
anchors: this.activeGeo.getAnchorLocals(),
|
|
62137
62427
|
object3D: this.activeGeoLine,
|
|
62138
62428
|
};
|
|
62139
62429
|
this.store.add(ann);
|
|
@@ -62146,6 +62436,61 @@ class SurfaceAnnotator {
|
|
|
62146
62436
|
this.removeGeoMarkers();
|
|
62147
62437
|
this.activeGeo = undefined;
|
|
62148
62438
|
this.activeGeoLine = undefined;
|
|
62439
|
+
this.geoClosed = false;
|
|
62440
|
+
this.emitInteraction();
|
|
62441
|
+
}
|
|
62442
|
+
/**
|
|
62443
|
+
* Re-open a committed geodesic contour for editing: rebuild its anchors (from the stored positions)
|
|
62444
|
+
* as draggable markers and reuse its line for live updates. No-op for non-geodesic / anchorless
|
|
62445
|
+
* annotations. The edit session ends on Enter / mode-switch / selecting another / Esc.
|
|
62446
|
+
*/
|
|
62447
|
+
tryOpenGeoEdit(id) {
|
|
62448
|
+
var _a;
|
|
62449
|
+
if (this.editingId === id)
|
|
62450
|
+
return;
|
|
62451
|
+
const a = this.store.get(id);
|
|
62452
|
+
if (!a || a.type !== "contour" || a.mode !== "geodesic" || !a.anchors || a.anchors.length < 2) {
|
|
62453
|
+
return;
|
|
62454
|
+
}
|
|
62455
|
+
// Discard a half-drawn (uncommitted) geodesic before entering the edit session.
|
|
62456
|
+
if (this.activeGeo && !this.editingId)
|
|
62457
|
+
this.clearActiveGeo();
|
|
62458
|
+
const indices = a.anchors.map((v) => this.graph.nearestVertex(new Vector3(v.x, v.y, v.z)));
|
|
62459
|
+
this.activeGeo = GeodesicContour.fromAnchors(this.graph, this.o.mesh, indices);
|
|
62460
|
+
this.activeGeoLine = (_a = a.object3D) !== null && _a !== void 0 ? _a : undefined;
|
|
62461
|
+
this.editingId = id;
|
|
62462
|
+
this.geoClosed = a.closed;
|
|
62463
|
+
this.rebuildGeoMarkers();
|
|
62464
|
+
this.redrawGeoLine();
|
|
62465
|
+
this.emitInteraction();
|
|
62466
|
+
}
|
|
62467
|
+
/**
|
|
62468
|
+
* Finalize an edit session on a committed geodesic: the annotation data was kept in sync live
|
|
62469
|
+
* (redrawGeoLine), so here we just remove the edit markers, notify subscribers, and end the
|
|
62470
|
+
* session — or delete the annotation if it was reduced below 2 anchors.
|
|
62471
|
+
*/
|
|
62472
|
+
recommitGeodesic() {
|
|
62473
|
+
const id = this.editingId;
|
|
62474
|
+
if (!id)
|
|
62475
|
+
return;
|
|
62476
|
+
if (!this.activeGeo || this.activeGeo.anchorCount < 2) {
|
|
62477
|
+
this.removeGeoMarkers();
|
|
62478
|
+
this.activeGeo = undefined;
|
|
62479
|
+
this.activeGeoLine = undefined; // owned by the annotation
|
|
62480
|
+
this.editingId = null;
|
|
62481
|
+
this.geoClosed = false;
|
|
62482
|
+
this.deleteAnnotation(id);
|
|
62483
|
+
this.emitInteraction();
|
|
62484
|
+
return;
|
|
62485
|
+
}
|
|
62486
|
+
this.redrawGeoLine(); // final data sync
|
|
62487
|
+
this.removeGeoMarkers();
|
|
62488
|
+
this.activeGeo = undefined;
|
|
62489
|
+
this.activeGeoLine = undefined; // owned by the annotation, do not dispose
|
|
62490
|
+
this.editingId = null;
|
|
62491
|
+
this.geoClosed = false;
|
|
62492
|
+
this.store.touch();
|
|
62493
|
+
this.emitInteraction();
|
|
62149
62494
|
}
|
|
62150
62495
|
isTypingTarget(e) {
|
|
62151
62496
|
const t = e.target;
|
|
@@ -62155,16 +62500,14 @@ class SurfaceAnnotator {
|
|
|
62155
62500
|
return tag === "INPUT" || tag === "TEXTAREA" || t.isContentEditable;
|
|
62156
62501
|
}
|
|
62157
62502
|
dispose() {
|
|
62158
|
-
var _a;
|
|
62159
62503
|
this.clearActiveGeo();
|
|
62160
|
-
(_a = this.geoHoverBadge) === null || _a === void 0 ? void 0 : _a.remove();
|
|
62161
|
-
this.geoHoverBadge = undefined;
|
|
62162
62504
|
window.removeEventListener("pointerdown", this.onPointerDown, true);
|
|
62163
62505
|
window.removeEventListener("pointermove", this.onPointerMove, true);
|
|
62164
62506
|
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
62165
62507
|
window.removeEventListener("keydown", this.onKeyDown);
|
|
62166
62508
|
window.removeEventListener("keyup", this.onKeyUp);
|
|
62167
62509
|
window.removeEventListener("resize", this.onResize);
|
|
62510
|
+
window.removeEventListener("contextmenu", this.onContextMenu, true);
|
|
62168
62511
|
}
|
|
62169
62512
|
}
|
|
62170
62513
|
SurfaceAnnotator.TAP_MS = 250;
|
|
@@ -83597,15 +83940,15 @@ function evaluateElement(element, weights) {
|
|
|
83597
83940
|
}
|
|
83598
83941
|
|
|
83599
83942
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
83600
|
-
// "v3.6.
|
|
83943
|
+
// "v3.6.10" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version.
|
|
83601
83944
|
// When copper3d is consumed from local source (no rollup replace step), the identifier is undefined and
|
|
83602
83945
|
// referencing it throws a ReferenceError — guard it so local loading still works.
|
|
83603
83946
|
let _revision = "unknown";
|
|
83604
83947
|
try {
|
|
83605
|
-
_revision = "v3.6.
|
|
83948
|
+
_revision = "v3.6.10";
|
|
83606
83949
|
}
|
|
83607
83950
|
catch (_a) {
|
|
83608
|
-
/* "v3.6.
|
|
83951
|
+
/* "v3.6.10" not injected (local source build) */
|
|
83609
83952
|
}
|
|
83610
83953
|
const REVISION = _revision;
|
|
83611
83954
|
// Expose on global so the version can be read in a production browser console via window.__COPPER3D_VERSION__
|