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.
@@ -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
- strokes stay where they were laid down while the model shifts out from under
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 **2**: both frames carry
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,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.87.0",
3
+ "version": "0.88.0",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,16 +1,16 @@
1
- // Viewbar chrome for annotation mode: the pencil toggle + contextual actions
2
- // (Undo / Clear / Send) shown while the mode is on. A direct sibling of
3
- // measure-controls.js same no-op-without-button contract, same attribute
4
- // restore discipline on detach. The mode object (annotate-mode.js) owns all
5
- // behavior; this file only puts it on screen. One extra contract: a host whose
6
- // markup HAS the button but whose mount passed no onAnnotationSend gets the
7
- // button hidden entirely (spec: no dead Send) — mount passes mode = null.
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
- // `send: "host"` drops the Send button from the row and leaves Undo/Clear.
10
- // It is for a host that draws its own send affordance (partforge-cloud pairs
11
- // the sketch with a typed prompt in its own composer, then calls
12
- // runtime.annotate.send()) two Send buttons in two places, one of which
13
- // ignores the typed message, is the failure this avoids.
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, send = "viewbar" } = {}) {
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, buttons.map((element) => ({ element })))
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
- undoButton.addEventListener("click", onUndo);
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),