@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.
- package/dist/{index-DENhglDF.d.ts → index-CMSOhoVs.d.ts} +6 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/model-target-BykKL8H5.js +350 -0
- package/dist/model.d.ts +52 -0
- package/dist/model.js +182 -0
- package/dist/{options-CNqXqkp9.d.ts → options-DPsmmgra.d.ts} +4 -0
- package/dist/plugins/next.d.ts +1 -1
- package/dist/plugins/vite.d.ts +1 -1
- package/dist/plugins/vite.js +61 -5
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{src-BsWnyGwB.js → src-B134WXZ8.js} +311 -284
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +2 -2
- package/dist/toolbar.iife.js +193 -36
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +1 -1
- package/package.json +7 -3
|
@@ -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 {
|
|
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
|
|
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
|
|
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 };
|
package/dist/model.d.ts
ADDED
|
@@ -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. */
|
package/dist/plugins/next.d.ts
CHANGED
package/dist/plugins/vite.d.ts
CHANGED