partforge 0.87.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/AUTHORING-PARTS.md +39 -3
- package/package.json +1 -1
- package/src/framework/annotate/annotate-controls.js +15 -57
- package/src/framework/annotate/annotate-mode.js +547 -68
- package/src/framework/annotate/elements.js +464 -0
- package/src/framework/annotate/ink-canvas.js +217 -60
- package/src/framework/annotate/sketch-toolbar.js +202 -0
- package/src/framework/app.css +64 -21
- package/src/framework/chrome.css +4 -2
- package/src/framework/mount.js +30 -7
- package/src/framework/oracle/annotation-ray.js +92 -0
- package/src/oracle.js +4 -0
- package/types/oracle.d.ts +3 -0
- package/types/testing.d.ts +15 -0
- package/src/framework/annotate/ink.js +0 -124
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -1810,7 +1810,7 @@ button are hidden while Sketch (annotate) mode is active, but that only governs
|
|
|
1810
1810
|
The ink is a transparent overlay and the WebGL canvas keeps rendering beneath
|
|
1811
1811
|
it, so a host that calls `runtime.projection.set()` mid-sketch **visibly
|
|
1812
1812
|
re-frames the 3D view underneath ink the user may still be drawing**: the
|
|
1813
|
-
|
|
1813
|
+
elements stay where they were laid down while the model shifts out from under
|
|
1814
1814
|
them, and the sketch that gets sent is misaligned, not merely mis-labelled.
|
|
1815
1815
|
Deliberately unguarded, the same way it's always been free to call
|
|
1816
1816
|
`setCameraState` during Sketch.
|
|
@@ -1821,10 +1821,46 @@ Deliberately unguarded, the same way it's always been free to call
|
|
|
1821
1821
|
(replays exactly against the build that produced it) and `parts` (pinned to
|
|
1822
1822
|
the CAD geometry, so it survives a later rebuild's bbox recentring; reread a
|
|
1823
1823
|
sketch's camera intrinsics from `parts`, not `world`, once the model has been
|
|
1824
|
-
rebuilt). `ANNOTATION_VERSION` is **
|
|
1824
|
+
rebuilt). `ANNOTATION_VERSION` is **3**: both frames carry
|
|
1825
1825
|
`projection: "perspective" | "orthographic"`, and under an orthographic camera
|
|
1826
1826
|
`fov` is `null` while `orthoHeight` gives the frustum's world height instead.
|
|
1827
|
-
(v1 had `fov` only, and predates the projection toggle
|
|
1827
|
+
(v1 had `fov` only, and predates the projection toggle; v3 replaced the
|
|
1828
|
+
payload's `strokes` array with `elements` — typed pen/line/rect/ellipse
|
|
1829
|
+
shapes rather than raw ink paths — a change orthogonal to this camera block.)
|
|
1830
|
+
The payload is self-describing for LLM consumers: a top-level `summary` joins
|
|
1831
|
+
every element's plain-language `description`, and `frames` is a legend mapping
|
|
1832
|
+
each payload path to its coordinate convention (element `params` are
|
|
1833
|
+
stage-space, anchor `screen`s are per-axis normalized 0..1, descriptions are
|
|
1834
|
+
viewport percentages — `viewport.aspect` bridges them). Each element carries an
|
|
1835
|
+
`id` (`"e1"`, `"e2"`, …) for unambiguous reference in replies, `rotDeg`
|
|
1836
|
+
alongside the radian `rot`, and erased spans rendered into the description in
|
|
1837
|
+
each type's own vocabulary ("erased top edge", "erased arc 36°–126°"); each
|
|
1838
|
+
anchor of a gapped element carries the `run` index of the visible fragment it
|
|
1839
|
+
sits on.
|
|
1840
|
+
|
|
1841
|
+
**Reconstructing rays from a sketch payload.** Every anchor also carries
|
|
1842
|
+
`ray: { origin, dir }` — a pick ray in the **parts frame** (mm origin, unit
|
|
1843
|
+
direction), computed from the live camera at send time and rounded to 4
|
|
1844
|
+
decimals; it is omitted when `camera.parts` is `null` (no meshes at send
|
|
1845
|
+
time — the same condition under which no `hit` can exist). Unlike `hit`,
|
|
1846
|
+
the ray is present even where the stroke crosses empty space, so any anchor
|
|
1847
|
+
can be projected onto a construction plane. For screen points that have no
|
|
1848
|
+
anchor (a circle's rim, a grid over a region), `partforge/oracle` exports
|
|
1849
|
+
`annotationRay(payload, screenOrAnchor, { frame? })` — the same ray,
|
|
1850
|
+
reconstructed from the payload's camera block (perspective and orthographic
|
|
1851
|
+
both) — and `rayPlane(ray, plane)` intersects either kind of ray with
|
|
1852
|
+
`{ point, normal }` or the shorthand origin planes `"xy" | "yz" | "zx"`,
|
|
1853
|
+
returning `{ point, t }` in mm or `null` on a parallel / behind-origin miss
|
|
1854
|
+
(the same miss semantics as `hit: null`). End to end:
|
|
1855
|
+
|
|
1856
|
+
```js
|
|
1857
|
+
import { annotationRay, rayPlane } from "partforge/oracle";
|
|
1858
|
+
const anchor = payload.elements.find((e) => e.id === "e3")
|
|
1859
|
+
.anchors.find((a) => a.at === "center");
|
|
1860
|
+
const hit = rayPlane(anchor.ray ?? annotationRay(payload, anchor), "xy");
|
|
1861
|
+
// → boss where the sketched circle's center points, on the z=0 plane:
|
|
1862
|
+
// k.prism({ points: circleProfile(r_mm, [hit.point[0], hit.point[1]]), h })
|
|
1863
|
+
```
|
|
1828
1864
|
|
|
1829
1865
|
**The markup convention (`demo.html` is the canonical copy-me page):** `<body>` carries
|
|
1830
1866
|
`class="pf-shell"`, the flex row that lays the viewer column next to the rail. `#app`
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// Viewbar chrome for annotation mode: the pencil toggle
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// Viewbar chrome for annotation mode: just the pencil toggle. A direct
|
|
2
|
+
// sibling of measure-controls.js — same no-op-without-button contract, same
|
|
3
|
+
// attribute restore discipline on detach. The mode object (annotate-mode.js)
|
|
4
|
+
// owns all behavior; this file only puts the toggle on screen. One extra
|
|
5
|
+
// contract: a host whose markup HAS the button but whose mount passed no
|
|
6
|
+
// onAnnotationSend gets the button hidden entirely (spec: no dead toggle) —
|
|
7
|
+
// mount passes mode = null.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// The Undo/Clear/Send actions that used to live in a row beside this button
|
|
10
|
+
// moved to the sketch toolbar (sketch-toolbar.js, spec 2026-08-27) — the
|
|
11
|
+
// toolbar OWNS the top of the stage while sketch mode is on, replacing the
|
|
12
|
+
// whole viewbar rather than sharing it. This file no longer knows about
|
|
13
|
+
// Undo/Clear/Send at all.
|
|
14
14
|
import { attachButtonTooltips } from "../tooltip.js";
|
|
15
15
|
import { runCleanupSteps, captureAttributes, restoreAttributes } from "../teardown.js";
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ const PENCIL_ICON = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none"
|
|
|
19
19
|
|
|
20
20
|
const noop = () => {};
|
|
21
21
|
|
|
22
|
-
export function attachAnnotateControls(viewer, mode, { annotate: button } = {}, { tooltip, escapeScope
|
|
22
|
+
export function attachAnnotateControls(viewer, mode, { annotate: button } = {}, { tooltip, escapeScope } = {}) {
|
|
23
23
|
if (!button) return { detach: noop };
|
|
24
24
|
|
|
25
25
|
const hostAttributes = captureAttributes(button, BUTTON_ATTRIBUTES);
|
|
@@ -42,33 +42,8 @@ export function attachAnnotateControls(viewer, mode, { annotate: button } = {},
|
|
|
42
42
|
button.setAttribute("aria-pressed", "false");
|
|
43
43
|
if (!tooltip && !button.hasAttribute("title")) button.title = "Sketch";
|
|
44
44
|
|
|
45
|
-
const actions = document.createElement("span");
|
|
46
|
-
actions.className = "pf-annotate-actions";
|
|
47
|
-
const undoButton = document.createElement("button");
|
|
48
|
-
undoButton.type = "button";
|
|
49
|
-
undoButton.textContent = "Undo";
|
|
50
|
-
undoButton.title = "Remove the last stroke";
|
|
51
|
-
undoButton.setAttribute("aria-label", "Remove the last stroke");
|
|
52
|
-
const clearButton = document.createElement("button");
|
|
53
|
-
clearButton.type = "button";
|
|
54
|
-
clearButton.textContent = "Clear";
|
|
55
|
-
clearButton.title = "Remove all strokes";
|
|
56
|
-
clearButton.setAttribute("aria-label", "Remove all strokes");
|
|
57
|
-
let sendButton = null;
|
|
58
|
-
if (send !== "host") {
|
|
59
|
-
sendButton = document.createElement("button");
|
|
60
|
-
sendButton.type = "button";
|
|
61
|
-
sendButton.className = "pf-annotate-send";
|
|
62
|
-
sendButton.textContent = "Send";
|
|
63
|
-
sendButton.title = "Send the annotation";
|
|
64
|
-
sendButton.setAttribute("aria-label", "Send the annotation");
|
|
65
|
-
}
|
|
66
|
-
actions.append(...[undoButton, clearButton, sendButton].filter(Boolean));
|
|
67
|
-
button.after(actions);
|
|
68
|
-
|
|
69
|
-
const buttons = [button, undoButton, clearButton, sendButton].filter(Boolean);
|
|
70
45
|
const tooltipBinding = tooltip
|
|
71
|
-
? attachButtonTooltips(tooltip,
|
|
46
|
+
? attachButtonTooltips(tooltip, [{ element: button }])
|
|
72
47
|
: null;
|
|
73
48
|
|
|
74
49
|
function sync() {
|
|
@@ -76,18 +51,10 @@ export function attachAnnotateControls(viewer, mode, { annotate: button } = {},
|
|
|
76
51
|
button.setAttribute("aria-pressed", String(on));
|
|
77
52
|
button.setAttribute("aria-label", on ? "Stop sketching" : "Sketch");
|
|
78
53
|
button.classList.toggle("on", on);
|
|
79
|
-
actions.hidden = !on;
|
|
80
|
-
const empty = mode.strokeCount() === 0;
|
|
81
|
-
undoButton.disabled = empty;
|
|
82
|
-
clearButton.disabled = empty;
|
|
83
|
-
if (sendButton) sendButton.disabled = empty;
|
|
84
54
|
tooltipBinding?.sync();
|
|
85
55
|
}
|
|
86
56
|
|
|
87
57
|
const onToggle = () => { mode.setEnabled(!mode.isEnabled()); sync(); };
|
|
88
|
-
const onUndo = () => { mode.undo(); sync(); };
|
|
89
|
-
const onClear = () => { mode.clear(); sync(); };
|
|
90
|
-
const onSendClick = () => { mode.send(); sync(); };
|
|
91
58
|
const onEscape = (event) => {
|
|
92
59
|
if (event.key !== "Escape" || !mode.isEnabled()) return;
|
|
93
60
|
event.preventDefault();
|
|
@@ -99,14 +66,10 @@ export function attachAnnotateControls(viewer, mode, { annotate: button } = {},
|
|
|
99
66
|
sync();
|
|
100
67
|
tooltipBinding?.hide();
|
|
101
68
|
};
|
|
102
|
-
const offInk = mode.onInkChange(sync);
|
|
103
69
|
const offMode = mode.onModeChange(sync);
|
|
104
70
|
|
|
105
71
|
button.addEventListener("click", onToggle);
|
|
106
|
-
|
|
107
|
-
clearButton.addEventListener("click", onClear);
|
|
108
|
-
sendButton?.addEventListener("click", onSendClick);
|
|
109
|
-
const escapeTargets = [escapeScope ?? viewer.domElement, ...buttons];
|
|
72
|
+
const escapeTargets = [escapeScope ?? viewer.domElement, button];
|
|
110
73
|
for (const element of escapeTargets) element.addEventListener("keydown", onEscape);
|
|
111
74
|
sync();
|
|
112
75
|
|
|
@@ -116,15 +79,10 @@ export function attachAnnotateControls(viewer, mode, { annotate: button } = {},
|
|
|
116
79
|
if (detached) return;
|
|
117
80
|
detached = true;
|
|
118
81
|
runCleanupSteps([
|
|
119
|
-
offInk,
|
|
120
82
|
offMode,
|
|
121
83
|
() => button.removeEventListener("click", onToggle),
|
|
122
|
-
() => undoButton.removeEventListener("click", onUndo),
|
|
123
|
-
() => clearButton.removeEventListener("click", onClear),
|
|
124
|
-
() => sendButton?.removeEventListener("click", onSendClick),
|
|
125
84
|
...escapeTargets.map((element) => () => element.removeEventListener("keydown", onEscape)),
|
|
126
85
|
() => tooltipBinding?.detach(),
|
|
127
|
-
() => actions.remove(),
|
|
128
86
|
() => restoreAttributes(button, hostAttributes),
|
|
129
87
|
() => { button.innerHTML = hostHtml; },
|
|
130
88
|
() => button.classList.toggle("on", hostOn),
|