partforge 0.86.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 +50 -4
- 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/capture-frame.js +118 -0
- package/src/framework/chrome.css +4 -2
- package/src/framework/mount.js +39 -8
- package/src/framework/oracle/annotation-ray.js +92 -0
- package/src/framework/viewer.js +39 -20
- package/src/oracle.js +4 -0
- package/types/index.d.ts +8 -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
|
@@ -1770,7 +1770,7 @@ framing offscreen at a resolution independent of the window size and devicePixel
|
|
|
1770
1770
|
for gallery/preview images, where grabbing the live canvas would be capped at the viewer
|
|
1771
1771
|
pane's pixel size:
|
|
1772
1772
|
|
|
1773
|
-
- `runtime.captureCurrent({ size = 2048, hideGrid = true, quality = 0.9 } = {}) → string | null` —
|
|
1773
|
+
- `runtime.captureCurrent({ size = 2048, hideGrid = true, quality = 0.9, recenter = false } = {}) → string | null` —
|
|
1774
1774
|
one offscreen render from the live camera's pose (position, up, and orbit target — not a
|
|
1775
1775
|
canonical pose) with the live viewport's aspect ratio, `size` px on the long edge
|
|
1776
1776
|
(clamped into `[256, maxTextureSize]`). Renders with 4× MSAA and the same
|
|
@@ -1783,6 +1783,16 @@ pane's pixel size:
|
|
|
1783
1783
|
in the scene, so a dimensioned capture needs no special handling — enable measure
|
|
1784
1784
|
mode (`runtime.measure.setEnabled(true)`) and call `captureCurrent()`; the dims are
|
|
1785
1785
|
just part of the rendered frame.
|
|
1786
|
+
`recenter: true` centres the part: the capture becomes the largest centred
|
|
1787
|
+
sub-window of the current framing that still holds every visible vertex (equal
|
|
1788
|
+
margins on both axes, rendered at the full `size` resolution through a view
|
|
1789
|
+
offset, so it is a pixel-exact crop of what the user framed — same
|
|
1790
|
+
perspective, no re-encode). The extent is the projection of the actual mesh
|
|
1791
|
+
vertices, not a bounding box, so it is exact at any angle. The framing is
|
|
1792
|
+
kept as-is when the geometry runs past any frame edge (a user who zoomed in
|
|
1793
|
+
past the part's silhouette cropped it on purpose), when it is already centred,
|
|
1794
|
+
or when measurement dimensions are pinned (their labels sit beside the part and
|
|
1795
|
+
could otherwise be cut off).
|
|
1786
1796
|
- `runtime.captureViews(viewNames) → [{ view, dataUrl }]` — the canonical-angle
|
|
1787
1797
|
counterpart (fixed poses, framed to the visible assembly, 1024², grid hidden). Sized
|
|
1788
1798
|
for feeding a vision model, not for display; use `captureCurrent` for showcase images.
|
|
@@ -1800,7 +1810,7 @@ button are hidden while Sketch (annotate) mode is active, but that only governs
|
|
|
1800
1810
|
The ink is a transparent overlay and the WebGL canvas keeps rendering beneath
|
|
1801
1811
|
it, so a host that calls `runtime.projection.set()` mid-sketch **visibly
|
|
1802
1812
|
re-frames the 3D view underneath ink the user may still be drawing**: the
|
|
1803
|
-
|
|
1813
|
+
elements stay where they were laid down while the model shifts out from under
|
|
1804
1814
|
them, and the sketch that gets sent is misaligned, not merely mis-labelled.
|
|
1805
1815
|
Deliberately unguarded, the same way it's always been free to call
|
|
1806
1816
|
`setCameraState` during Sketch.
|
|
@@ -1811,10 +1821,46 @@ Deliberately unguarded, the same way it's always been free to call
|
|
|
1811
1821
|
(replays exactly against the build that produced it) and `parts` (pinned to
|
|
1812
1822
|
the CAD geometry, so it survives a later rebuild's bbox recentring; reread a
|
|
1813
1823
|
sketch's camera intrinsics from `parts`, not `world`, once the model has been
|
|
1814
|
-
rebuilt). `ANNOTATION_VERSION` is **
|
|
1824
|
+
rebuilt). `ANNOTATION_VERSION` is **3**: both frames carry
|
|
1815
1825
|
`projection: "perspective" | "orthographic"`, and under an orthographic camera
|
|
1816
1826
|
`fov` is `null` while `orthoHeight` gives the frustum's world height instead.
|
|
1817
|
-
(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
|
+
```
|
|
1818
1864
|
|
|
1819
1865
|
**The markup convention (`demo.html` is the canonical copy-me page):** `<body>` carries
|
|
1820
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),
|