@autono/pinbox-toolbar 0.19.0 → 0.21.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.
@@ -69,6 +69,8 @@ interface ToolbarState {
69
69
  * Persisted per endpoint; the bar's camera button flips it.
70
70
  */
71
71
  captureMode: "dom" | "tab";
72
+ /** Optional model camera action label; absent preserves the 2D capture toggle. */
73
+ captureLabel?: string | undefined;
72
74
  }
73
75
  interface Store {
74
76
  get(): ToolbarState;
@@ -148,6 +150,9 @@ declare class PinboxToolbarElement extends BaseElement {
148
150
  restore(keyboard?: boolean): void;
149
151
  }
150
152
  //#endregion
153
+ //#region src/previews.d.ts
154
+ declare function mountPreviewSwitcher(host: HTMLElement, endpoint: string): void;
155
+ //#endregion
151
156
  //#region src/transport/mirror.d.ts
152
157
  interface StorageLike {
153
158
  getItem(key: string): string | null;
@@ -281,4 +286,4 @@ declare const Pinbox: {
281
286
  init(config: PinboxConfig): PinboxToolbarElement;
282
287
  };
283
288
  //#endregion
284
- export { applyHubEvent as _, HubEvent as a, upsertPin as b, WebSocketLike as c, PinboxToolbarElement as d, Draft as f, appendThreadMessage as g, UiStatus as h, ConnectionState as i, HubError as l, ToolbarState as m, PinboxConfig as n, HubTransport as o, Store as p, defineToolbarElement as r, TransportOptions as s, Pinbox as t, StorageLike as u, createStore as v, CaptureResult as x, deriveUiStatus as y };
289
+ export { CaptureResult as S, appendThreadMessage as _, HubEvent as a, deriveUiStatus as b, WebSocketLike as c, mountPreviewSwitcher as d, PinboxToolbarElement as f, UiStatus as g, ToolbarState as h, ConnectionState as i, HubError as l, Store as m, PinboxConfig as n, HubTransport as o, Draft as p, defineToolbarElement as r, TransportOptions as s, Pinbox as t, StorageLike as u, applyHubEvent as v, upsertPin as x, createStore as y };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as applyHubEvent, a as HubEvent, b as upsertPin, c as WebSocketLike, d as PinboxToolbarElement, f as Draft, g as appendThreadMessage, h as UiStatus, i as ConnectionState, l as HubError, m as ToolbarState, n as PinboxConfig, o as HubTransport, p as Store, r as defineToolbarElement, s as TransportOptions, t as Pinbox, u as StorageLike, v as createStore, x as CaptureResult, y as deriveUiStatus } from "./index-DENhglDF.js";
2
- export { type CaptureResult, type ConnectionState, type Draft, HubError, type HubEvent, HubTransport, Pinbox, PinboxConfig, PinboxToolbarElement, type StorageLike, type Store, type ToolbarState, type TransportOptions, type UiStatus, type WebSocketLike, appendThreadMessage, applyHubEvent, createStore, defineToolbarElement, deriveUiStatus, upsertPin };
1
+ import { S as CaptureResult, _ as appendThreadMessage, a as HubEvent, b as deriveUiStatus, c as WebSocketLike, d as mountPreviewSwitcher, f as PinboxToolbarElement, g as UiStatus, h as ToolbarState, i as ConnectionState, l as HubError, m as Store, n as PinboxConfig, o as HubTransport, p as Draft, r as defineToolbarElement, s as TransportOptions, t as Pinbox, u as StorageLike, v as applyHubEvent, x as upsertPin, y as createStore } from "./index-CMSOhoVs.js";
2
+ export { type CaptureResult, type ConnectionState, type Draft, HubError, type HubEvent, HubTransport, Pinbox, PinboxConfig, PinboxToolbarElement, type StorageLike, type Store, type ToolbarState, type TransportOptions, type UiStatus, type WebSocketLike, appendThreadMessage, applyHubEvent, createStore, defineToolbarElement, deriveUiStatus, mountPreviewSwitcher, upsertPin };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as HubError, c as createStore, i as HubTransport, l as deriveUiStatus, n as defineToolbarElement, o as appendThreadMessage, r as PinboxToolbarElement, s as applyHubEvent, t as Pinbox, u as upsertPin } from "./src-BsWnyGwB.js";
2
- export { HubError, HubTransport, Pinbox, PinboxToolbarElement, appendThreadMessage, applyHubEvent, createStore, defineToolbarElement, deriveUiStatus, upsertPin };
1
+ import { a as HubTransport, c as applyHubEvent, d as upsertPin, i as PinboxToolbarElement, l as createStore, n as defineToolbarElement, o as HubError, r as mountPreviewSwitcher, s as appendThreadMessage, t as Pinbox, u as deriveUiStatus } from "./src-B134WXZ8.js";
2
+ export { HubError, HubTransport, Pinbox, PinboxToolbarElement, appendThreadMessage, applyHubEvent, createStore, defineToolbarElement, deriveUiStatus, mountPreviewSwitcher, upsertPin };
@@ -0,0 +1,350 @@
1
+ //#region src/capture-mode.ts
2
+ function captureKey(prefix) {
3
+ return `${prefix}:capture`;
4
+ }
5
+ function loadCaptureMode(storage, key, fallback) {
6
+ try {
7
+ const raw = storage?.getItem(key);
8
+ if (raw === "dom" || raw === "tab") return raw;
9
+ } catch {}
10
+ return fallback;
11
+ }
12
+ function saveCaptureMode(storage, key, mode) {
13
+ try {
14
+ storage?.setItem(key, mode);
15
+ } catch {}
16
+ }
17
+ //#endregion
18
+ //#region src/model-capture.ts
19
+ const handlers = /* @__PURE__ */ new WeakMap();
20
+ /** The first viewer owns the camera until another registered surface is entered. */
21
+ function registerModelCapture(owner, run, error) {
22
+ const entry = {
23
+ run,
24
+ error,
25
+ busy: false
26
+ };
27
+ const entries = handlers.get(owner) ?? [];
28
+ entries.push(entry);
29
+ handlers.set(owner, entries);
30
+ owner.store.update({ captureLabel: "Capture model screenshot (S)" });
31
+ return {
32
+ activate() {
33
+ const index = entries.indexOf(entry);
34
+ if (index < 0) return;
35
+ entries.splice(index, 1);
36
+ entries.unshift(entry);
37
+ },
38
+ destroy() {
39
+ const index = entries.indexOf(entry);
40
+ if (index < 0) return;
41
+ entries.splice(index, 1);
42
+ if (!entries.length) {
43
+ handlers.delete(owner);
44
+ owner.store.update({ captureLabel: void 0 });
45
+ }
46
+ }
47
+ };
48
+ }
49
+ function runModelCapture(owner) {
50
+ const entries = handlers.get(owner);
51
+ const entry = entries?.[0];
52
+ if (!entries || !entry) return false;
53
+ if (entries.some((candidate) => candidate.busy)) return true;
54
+ entry.busy = true;
55
+ try {
56
+ Promise.resolve(entry.run()).catch(entry.error).finally(() => {
57
+ entry.busy = false;
58
+ });
59
+ } catch (error) {
60
+ entry.busy = false;
61
+ entry.error(error);
62
+ }
63
+ return true;
64
+ }
65
+ /** Shared by the camera button, puck and S shortcut; preserve the 2D fallback. */
66
+ function toggleToolbarCapture(owner, endpoint, release) {
67
+ if (runModelCapture(owner)) return true;
68
+ const next = owner.store.get().captureMode === "tab" ? "dom" : "tab";
69
+ owner.store.update({ captureMode: next });
70
+ if (next === "dom") release();
71
+ saveCaptureMode(globalThis.localStorage, captureKey(`pinbox:${endpoint}`), next);
72
+ return true;
73
+ }
74
+ //#endregion
75
+ //#region src/targeting/dom.ts
76
+ /**
77
+ * Deepest element under (clientX, clientY) that the caller does not ignore, or null when there is
78
+ * nothing there but page chrome (html/body).
79
+ *
80
+ * Looks THROUGH our own overlay rather than giving up at it. The single-element form could not:
81
+ * the drag-aim grip sits exactly on the point being aimed at, so it is always the topmost thing
82
+ * under the crosshair, and every probe came back "nothing" the moment touch aiming existed.
83
+ */
84
+ function hitTest(doc, x, y, ignore) {
85
+ const stack = doc.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
86
+ for (const el of stack) {
87
+ if (!el || el === doc.body || el === doc.documentElement) return null;
88
+ if (!ignore(el)) return el;
89
+ }
90
+ return null;
91
+ }
92
+ /** CLASS-or-TAG display name with a sibling index when needed (prototype nodeName). */
93
+ function nodeName(el) {
94
+ const key = el.classList[0];
95
+ let name = (key ?? el.tagName).toUpperCase();
96
+ const parent = el.parentElement;
97
+ if (parent) {
98
+ const sibs = [...parent.children].filter((c) => c.classList[0] === key && c.tagName === el.tagName);
99
+ if (sibs.length > 1) name += ` ${sibs.indexOf(el) + 1}`;
100
+ }
101
+ return name;
102
+ }
103
+ /**
104
+ * Human label for a target: an explicit data-pb-el annotation wins; otherwise a
105
+ * CLASS/TAG ancestry chain of at most 3 parts joined with ›, terminating early
106
+ * at the first annotated ancestor.
107
+ */
108
+ function targetLabel(el) {
109
+ const own = el.getAttribute("data-pb-el");
110
+ if (own) return own;
111
+ const parts = [nodeName(el)];
112
+ const body = el.ownerDocument.body;
113
+ let node = el.parentElement;
114
+ while (node && node !== body && parts.length < 3) {
115
+ const anchor = node.getAttribute("data-pb-el");
116
+ if (anchor) {
117
+ parts.unshift(anchor);
118
+ break;
119
+ }
120
+ if (node.classList[0]) parts.unshift(nodeName(node));
121
+ node = node.parentElement;
122
+ }
123
+ return parts.join(" › ");
124
+ }
125
+ const SAFE_ID = /^[A-Za-z][\w-]*$/;
126
+ /** Data attributes trusted as stable hooks, in priority order. */
127
+ const STABLE_DATA_ATTRS = [
128
+ "data-pb-anchor",
129
+ "data-pb-el",
130
+ "data-testid"
131
+ ];
132
+ function attrSegment(el, doc) {
133
+ for (const attr of STABLE_DATA_ATTRS) {
134
+ const value = el.getAttribute(attr);
135
+ if (value === null || value.includes("\"") || value.includes("\\")) continue;
136
+ const selector = `${el.tagName.toLowerCase()}[${attr}="${value}"]`;
137
+ if (doc.querySelectorAll(selector).length === 1) return selector;
138
+ }
139
+ return null;
140
+ }
141
+ function nthSegment(el) {
142
+ const tag = el.tagName.toLowerCase();
143
+ const parent = el.parentElement;
144
+ if (!parent) return tag;
145
+ const sameTag = [...parent.children].filter((c) => c.tagName === el.tagName);
146
+ return sameTag.length > 1 ? `${tag}:nth-of-type(${sameTag.indexOf(el) + 1})` : tag;
147
+ }
148
+ /**
149
+ * Stable CSS path for an element: ids > stable data attributes > an
150
+ * nth-of-type chain. Guaranteed round-trip: querySelector(buildSelector(el)) === el.
151
+ */
152
+ function buildSelector(el) {
153
+ const doc = el.ownerDocument;
154
+ const segments = [];
155
+ let node = el;
156
+ while (node && node !== doc.documentElement) {
157
+ const id = node.getAttribute("id");
158
+ if (id && SAFE_ID.test(id) && doc.querySelectorAll(`#${id}`).length === 1) {
159
+ segments.unshift(`#${id}`);
160
+ return segments.join(" > ");
161
+ }
162
+ const byAttr = attrSegment(node, doc);
163
+ if (byAttr) {
164
+ segments.unshift(byAttr);
165
+ return segments.join(" > ");
166
+ }
167
+ segments.unshift(nthSegment(node));
168
+ node = node.parentElement;
169
+ }
170
+ return segments.join(" > ");
171
+ }
172
+ //#endregion
173
+ //#region src/capture.ts
174
+ /** Curated computed-style subset — enough to reconstruct layout intent, tiny on the wire. */
175
+ const STYLE_KEYS = [
176
+ "display",
177
+ "position",
178
+ "font-size",
179
+ "color",
180
+ "background-color",
181
+ "margin",
182
+ "padding",
183
+ "overflow"
184
+ ];
185
+ const NEARBY_TEXT_MAX = 160;
186
+ function styleSubset(win, el) {
187
+ let cs;
188
+ try {
189
+ cs = win.getComputedStyle(el);
190
+ } catch {
191
+ return;
192
+ }
193
+ const out = {};
194
+ for (const key of STYLE_KEYS) {
195
+ const value = cs.getPropertyValue(key);
196
+ if (value !== "") out[key] = value;
197
+ }
198
+ return Object.keys(out).length > 0 ? out : void 0;
199
+ }
200
+ function ariaMap(el) {
201
+ const out = {};
202
+ for (const name of el.getAttributeNames()) if (name.startsWith("aria-")) out[name] = el.getAttribute(name) ?? "";
203
+ return Object.keys(out).length > 0 ? out : void 0;
204
+ }
205
+ function nearbyText(el) {
206
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
207
+ return text === "" ? void 0 : text.slice(0, NEARBY_TEXT_MAX);
208
+ }
209
+ /** The user's selection, only when it intersects the captured element. */
210
+ function selectedText(win, el) {
211
+ try {
212
+ const sel = win.getSelection?.();
213
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0) return void 0;
214
+ if (!sel.getRangeAt(0).intersectsNode(el)) return void 0;
215
+ const text = sel.toString().trim();
216
+ return text === "" ? void 0 : text;
217
+ } catch {
218
+ return;
219
+ }
220
+ }
221
+ /** `fixed` detected via ancestry: any ancestor with computed position: fixed. */
222
+ function isFixed(win, el) {
223
+ for (let node = el; node !== null; node = node.parentElement) try {
224
+ if (win.getComputedStyle(node).position === "fixed") return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ return false;
229
+ }
230
+ /** Beyond this an element is not a thing you pinned, it is a region. Too many to rewrite as a set. */
231
+ const MAX_RUNS = 40;
232
+ const MAX_RUN_LENGTH = 200;
233
+ /** Text that is not content: a script body or a stylesheet is not something to rewrite. */
234
+ const NON_CONTENT = /* @__PURE__ */ new Set([
235
+ "SCRIPT",
236
+ "STYLE",
237
+ "NOSCRIPT",
238
+ "TEMPLATE"
239
+ ]);
240
+ /**
241
+ * The element's text, split the way the browser stores it: one entry per run of characters.
242
+ *
243
+ * This walks TEXT NODES, not elements, and that distinction is the whole point — it makes no
244
+ * assumption about how a site is built. A heading is one run. A nav bar is one per link. A
245
+ * paragraph with a bold word in the middle is three, in reading order, including the halves either
246
+ * side of the bold. An earlier version keyed off "elements with no element children", which
247
+ * quietly lost the "Hello " in `<p>Hello <b>world</b></p>` — text a person can obviously see and
248
+ * would obviously expect to be able to change.
249
+ *
250
+ * `nearbyText` runs them all together, which is fine to read and useless to edit: it cannot tell
251
+ * an agent that "work approach people contact" is four separate places. This can.
252
+ */
253
+ function textRuns(el) {
254
+ const runs = [];
255
+ const walk = (node) => {
256
+ if (node.nodeType === 3) {
257
+ const text = (node.nodeValue ?? "").trim();
258
+ if (text.length > 0) runs.push(text.slice(0, MAX_RUN_LENGTH));
259
+ return runs.length <= MAX_RUNS;
260
+ }
261
+ if (node.nodeType !== 1 || NON_CONTENT.has(node.tagName)) return true;
262
+ for (const child of node.childNodes) if (!walk(child)) return false;
263
+ return true;
264
+ };
265
+ if (!walk(el) || runs.length === 0) return void 0;
266
+ return runs;
267
+ }
268
+ function buildContext(win, el) {
269
+ const context = {};
270
+ if (el.classList.length > 0) context.classes = [...el.classList];
271
+ const styles = styleSubset(win, el);
272
+ if (styles !== void 0) context.styles = styles;
273
+ const aria = ariaMap(el);
274
+ if (aria !== void 0) context.aria = aria;
275
+ const nearby = nearbyText(el);
276
+ if (nearby !== void 0) context.nearbyText = nearby;
277
+ const selected = selectedText(win, el);
278
+ if (selected !== void 0) context.selectedText = selected;
279
+ const runs = textRuns(el);
280
+ if (runs !== void 0) context.textRuns = runs;
281
+ return Object.keys(context).length > 0 ? context : void 0;
282
+ }
283
+ /** Fills PinInput.target/env from a chosen element (shapes come from the pin schema). */
284
+ function captureTarget(el, opts) {
285
+ const win = el.ownerDocument.defaultView;
286
+ const r = el.getBoundingClientRect();
287
+ const target = {
288
+ url: win.location.href,
289
+ selector: buildSelector(el),
290
+ tag: el.tagName.toLowerCase(),
291
+ rect: {
292
+ x: r.left + win.scrollX,
293
+ y: r.top + win.scrollY,
294
+ width: r.width,
295
+ height: r.height
296
+ },
297
+ fixed: isFixed(win, el)
298
+ };
299
+ if (opts?.anchor !== void 0) target.anchor = opts.anchor;
300
+ if (opts?.at !== void 0 && r.width > 0 && r.height > 0) {
301
+ const fx = (opts.at.x - (r.left + win.scrollX)) / r.width;
302
+ const fy = (opts.at.y - (r.top + win.scrollY)) / r.height;
303
+ if (fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1) target.spot = {
304
+ x: fx,
305
+ y: fy
306
+ };
307
+ }
308
+ const context = buildContext(win, el);
309
+ if (context !== void 0) target.context = context;
310
+ return {
311
+ target,
312
+ env: {
313
+ viewport: {
314
+ w: win.innerWidth,
315
+ h: win.innerHeight,
316
+ dpr: win.devicePixelRatio
317
+ },
318
+ browser: win.navigator.userAgent,
319
+ os: win.navigator.platform || "unknown",
320
+ colorScheme: win.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
321
+ }
322
+ };
323
+ }
324
+ //#endregion
325
+ //#region src/model-target.ts
326
+ const projectors = /* @__PURE__ */ new WeakMap();
327
+ function registerModelProjection(doc, project) {
328
+ const set = projectors.get(doc) ?? /* @__PURE__ */ new Set();
329
+ const entry = (anchor) => project(anchor);
330
+ set.add(entry);
331
+ projectors.set(doc, set);
332
+ return () => {
333
+ set.delete(entry);
334
+ if (!set.size) projectors.delete(doc);
335
+ };
336
+ }
337
+ function projectModelTarget(doc, anchor) {
338
+ for (const project of projectors.get(doc) ?? []) {
339
+ const p = project(anchor);
340
+ if (p && Number.isFinite(p.x) && Number.isFinite(p.y)) return {
341
+ x: p.x,
342
+ y: p.y,
343
+ width: 0,
344
+ height: 0
345
+ };
346
+ }
347
+ return null;
348
+ }
349
+ //#endregion
350
+ export { targetLabel as a, captureKey as c, hitTest as i, loadCaptureMode as l, registerModelProjection as n, registerModelCapture as o, captureTarget as r, toggleToolbarCapture as s, projectModelTarget as t };
@@ -0,0 +1,52 @@
1
+ import { f as PinboxToolbarElement } from "./index-CMSOhoVs.js";
2
+ import { ModelAnchor, ModelAnchor as ModelAnchor$1, Pin } from "@autono/pinbox-core/schema";
3
+ //#region src/model-target.d.ts
4
+ type ModelProjection = (anchor: ModelAnchor$1) => {
5
+ x: number;
6
+ y: number;
7
+ } | null;
8
+ //#endregion
9
+ //#region src/model-ui.d.ts
10
+ type ModelViewerAdapter = {
11
+ /** Canvas or element containing the model. DOM pins outside it keep working. */
12
+ surface: HTMLElement;
13
+ /** Raycast a viewport point; return a part-local anchor, or null for empty space. */
14
+ pick: (clientX: number, clientY: number) => ModelAnchor$1 | null;
15
+ /** Project to viewport CSS pixels. Null for a stale, hidden or occluded anchor. */
16
+ project: ModelProjection;
17
+ /** Called each update in pin mode, or null when the hover clears. */
18
+ onHover?: (anchor: ModelAnchor$1 | null) => void;
19
+ /** Filter activation for multiple viewers; may accept older revisions for loading. */
20
+ acceptsAnchor?: (anchor: ModelAnchor$1) => boolean;
21
+ /** Animate/load a pin view. Stop stale asynchronous work when signal is aborted. */
22
+ onActivate?: (anchor: ModelAnchor$1, pin: Pin, context: {
23
+ signal: AbortSignal;
24
+ }) => void | Promise<void>;
25
+ /** Camera button and S action. Host owns capture, preview, save and chat/storage UI. */
26
+ onCapture?: () => void | Promise<void>;
27
+ onError?: (error: unknown) => void;
28
+ };
29
+ /** Connect the real Pinbox toolbar, draft card and needle markers to a 3D viewer.
30
+ * Call update() after rendering a frame. Destroy before removing the viewer. */
31
+ declare function attachModelViewer(toolbar: PinboxToolbarElement, adapter: ModelViewerAdapter): {
32
+ openPin: (id: string) => Promise<void>;
33
+ update(): void;
34
+ destroy(): void;
35
+ };
36
+ //#endregion
37
+ //#region src/model.d.ts
38
+ type ModelScene = {
39
+ modelId: string;
40
+ revision: string;
41
+ /** Column-major 4x4 object-to-world matrix (e.g. Three.js matrixWorld.elements). */
42
+ partMatrix: (partId: string) => readonly number[] | undefined;
43
+ };
44
+ type AnchorResolution = {
45
+ status: "visible";
46
+ worldPosition: [number, number, number];
47
+ } | {
48
+ status: "stale" | "missing";
49
+ };
50
+ declare function resolveModelAnchor(anchor: ModelAnchor, scene: ModelScene): AnchorResolution;
51
+ //#endregion
52
+ export { AnchorResolution, type ModelAnchor, ModelScene, type ModelViewerAdapter, attachModelViewer, resolveModelAnchor };
package/dist/model.js ADDED
@@ -0,0 +1,182 @@
1
+ import { n as registerModelProjection, o as registerModelCapture, r as captureTarget } from "./model-target-BykKL8H5.js";
2
+ //#region src/model-hooks.ts
3
+ /** Renderer callbacks stay separate from placement and marker projection. */
4
+ function attachModelHooks(toolbar, adapter) {
5
+ const surface = adapter.surface, win = surface.ownerDocument.defaultView;
6
+ if (!win) throw new Error("The model surface must belong to a browser document");
7
+ const error = adapter.onError ?? ((cause) => console.error("Pinbox model hook failed", cause));
8
+ const capture = adapter.onCapture ? registerModelCapture(toolbar, adapter.onCapture, error) : void 0;
9
+ let point, hovering = false, disposed = false;
10
+ let active = null, controller;
11
+ let activation = Promise.resolve();
12
+ const clear = () => {
13
+ point = void 0;
14
+ if (hovering) {
15
+ hovering = false;
16
+ adapter.onHover?.(null);
17
+ }
18
+ };
19
+ const move = (event) => {
20
+ if (!event.composedPath().includes(surface)) {
21
+ clear();
22
+ return;
23
+ }
24
+ point = {
25
+ x: event.clientX,
26
+ y: event.clientY
27
+ };
28
+ capture?.activate();
29
+ };
30
+ const focus = () => capture?.activate();
31
+ const activate = (pin) => {
32
+ controller?.abort();
33
+ controller = new AbortController();
34
+ const signal = controller.signal, anchor = pin.target?.model;
35
+ if (!anchor || !adapter.onActivate || adapter.acceptsAnchor && !adapter.acceptsAnchor(anchor)) return Promise.resolve();
36
+ try {
37
+ return Promise.resolve(adapter.onActivate(anchor, pin, { signal })).catch((cause) => {
38
+ if (!signal.aborted) error(cause);
39
+ });
40
+ } catch (cause) {
41
+ error(cause);
42
+ return Promise.resolve();
43
+ }
44
+ };
45
+ const unsubscribe = toolbar.store.subscribe((state) => {
46
+ if (state.mode !== "placing" && hovering) {
47
+ hovering = false;
48
+ adapter.onHover?.(null);
49
+ }
50
+ if (active === state.activePinId) return;
51
+ const pin = state.pins.find((p) => p.id === state.activePinId);
52
+ if (state.activePinId && !pin) return;
53
+ active = state.activePinId;
54
+ controller?.abort();
55
+ if (pin) activation = activate(pin);
56
+ });
57
+ win.addEventListener("pointermove", move, true);
58
+ win.addEventListener("blur", clear);
59
+ surface.addEventListener("pointerleave", clear);
60
+ surface.addEventListener("pointerdown", focus, true);
61
+ return {
62
+ update() {
63
+ if (disposed || !adapter.onHover || !point || toolbar.store.get().mode !== "placing") return;
64
+ try {
65
+ const anchor = adapter.pick(point.x, point.y);
66
+ hovering = anchor !== null;
67
+ adapter.onHover(anchor);
68
+ } catch (cause) {
69
+ clear();
70
+ error(cause);
71
+ }
72
+ },
73
+ async openPin(id) {
74
+ if (disposed) return;
75
+ const pin = toolbar.store.get().pins.find((p) => p.id === id);
76
+ if (!pin) throw new Error("Pin not found.");
77
+ if (active === id) activation = activate(pin);
78
+ toolbar.store.update({
79
+ mode: "idle",
80
+ activePinId: id,
81
+ pinsHidden: false
82
+ });
83
+ await activation;
84
+ },
85
+ destroy() {
86
+ if (disposed) return;
87
+ disposed = true;
88
+ controller?.abort();
89
+ unsubscribe();
90
+ clear();
91
+ capture?.destroy();
92
+ win.removeEventListener("pointermove", move, true);
93
+ win.removeEventListener("blur", clear);
94
+ surface.removeEventListener("pointerleave", clear);
95
+ surface.removeEventListener("pointerdown", focus, true);
96
+ }
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region src/model-ui.ts
101
+ /** Connect the real Pinbox toolbar, draft card and needle markers to a 3D viewer.
102
+ * Call update() after rendering a frame. Destroy before removing the viewer. */
103
+ function attachModelViewer(toolbar, adapter) {
104
+ const doc = adapter.surface.ownerDocument, win = doc.defaultView;
105
+ if (!win) throw new Error("The model surface must belong to a browser document");
106
+ const unregister = registerModelProjection(doc, adapter.project);
107
+ const hooks = attachModelHooks(toolbar, adapter);
108
+ let down;
109
+ const pointerDown = (e) => {
110
+ if (e.composedPath().includes(adapter.surface)) down = {
111
+ x: e.clientX,
112
+ y: e.clientY
113
+ };
114
+ };
115
+ const click = (e) => {
116
+ if (toolbar.store.get().mode !== "placing" || e.composedPath().includes(toolbar) || !e.composedPath().includes(adapter.surface)) return;
117
+ e.preventDefault();
118
+ e.stopImmediatePropagation();
119
+ if (down && Math.hypot(e.clientX - down.x, e.clientY - down.y) > 5) {
120
+ down = void 0;
121
+ return;
122
+ }
123
+ down = void 0;
124
+ const model = adapter.pick(e.clientX, e.clientY);
125
+ if (!model) return;
126
+ const at = {
127
+ x: e.clientX + win.scrollX,
128
+ y: e.clientY + win.scrollY
129
+ };
130
+ const captured = captureTarget(adapter.surface, { at });
131
+ captured.target.model = model;
132
+ captured.target.anchor = `3D · ${model.partId}`;
133
+ delete captured.target.spot;
134
+ toolbar.store.place({
135
+ target: captured,
136
+ placedAt: at
137
+ });
138
+ };
139
+ win.addEventListener("pointerdown", pointerDown, true);
140
+ win.addEventListener("click", click, true);
141
+ let destroyed = false;
142
+ return {
143
+ openPin: hooks.openPin,
144
+ update() {
145
+ if (destroyed || !toolbar.isConnected) return;
146
+ hooks.update();
147
+ const s = toolbar.store.get();
148
+ if (s.pins.some((p) => p.target?.model) || s.draft?.target.target.model) toolbar.store.update({ clock: s.clock });
149
+ },
150
+ destroy() {
151
+ if (destroyed) return;
152
+ destroyed = true;
153
+ hooks.destroy();
154
+ unregister();
155
+ win.removeEventListener("pointerdown", pointerDown, true);
156
+ win.removeEventListener("click", click, true);
157
+ }
158
+ };
159
+ }
160
+ //#endregion
161
+ //#region src/model.ts
162
+ function resolveModelAnchor(anchor, scene) {
163
+ if (anchor.modelId !== scene.modelId || anchor.revision !== scene.revision) return { status: "stale" };
164
+ const values = scene.partMatrix(anchor.partId);
165
+ if (values?.length !== 16 || !values.every(Number.isFinite)) return { status: "missing" };
166
+ const m = values;
167
+ const [x, y, z] = anchor.position;
168
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15];
169
+ if (!Number.isFinite(w) || Math.abs(w) < 1e-12) return { status: "missing" };
170
+ const worldPosition = [
171
+ (m[0] * x + m[4] * y + m[8] * z + m[12]) / w,
172
+ (m[1] * x + m[5] * y + m[9] * z + m[13]) / w,
173
+ (m[2] * x + m[6] * y + m[10] * z + m[14]) / w
174
+ ];
175
+ if (!worldPosition.every(Number.isFinite)) return { status: "missing" };
176
+ return {
177
+ status: "visible",
178
+ worldPosition
179
+ };
180
+ }
181
+ //#endregion
182
+ export { attachModelViewer, resolveModelAnchor };
@@ -3,6 +3,10 @@
3
3
  interface PinboxPluginOptions {
4
4
  /** Explicit hub base URL. When set, `.pinbox/server.json` discovery is skipped entirely. */
5
5
  hub?: string;
6
+ /** Vite only: show registered local worktree previews. Opt-in, dev-only. */
7
+ previews?: boolean;
8
+ /** Local CLI executable used for preview discovery. Defaults to pinbox on PATH. */
9
+ previewExecutable?: string;
6
10
  /** Directory containing `.pinbox/server.json`. Defaults to `process.cwd()`. */
7
11
  projectRoot?: string;
8
12
  /** Spawn `pinbox serve` when no healthy hub is found. Defaults to true. */
@@ -1,4 +1,4 @@
1
- import { t as PinboxPluginOptions } from "../options-CNqXqkp9.js";
1
+ import { t as PinboxPluginOptions } from "../options-DPsmmgra.js";
2
2
  //#region src/plugins/next.d.ts
3
3
  /**
4
4
  * Structural stand-in for Next's `NextConfig`.
@@ -1,4 +1,4 @@
1
- import { t as PinboxPluginOptions } from "../options-CNqXqkp9.js";
1
+ import { t as PinboxPluginOptions } from "../options-DPsmmgra.js";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/plugins/vite.d.ts
4
4
  /**