@oeave/bakery3 0.0.0-stage → 0.2.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +562 -2
  3. package/dist/animation-B1h0Ryvj.d.ts +97 -0
  4. package/dist/bake/index.d.ts +369 -0
  5. package/dist/bake/index.js +9 -0
  6. package/dist/bake/index.js.map +1 -0
  7. package/dist/bake-DZ-CJR6f.d.ts +1364 -0
  8. package/dist/catalog/index.d.ts +100 -0
  9. package/dist/catalog/index.js +154 -0
  10. package/dist/catalog/index.js.map +1 -0
  11. package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
  12. package/dist/chunk-3G5QL4F4.js +145 -0
  13. package/dist/chunk-3G5QL4F4.js.map +1 -0
  14. package/dist/chunk-4E5VV4QY.js +12 -0
  15. package/dist/chunk-4E5VV4QY.js.map +1 -0
  16. package/dist/chunk-62M6XXNX.js +142 -0
  17. package/dist/chunk-62M6XXNX.js.map +1 -0
  18. package/dist/chunk-6FYI6AJM.js +1157 -0
  19. package/dist/chunk-6FYI6AJM.js.map +1 -0
  20. package/dist/chunk-6NYR73Y7.js +832 -0
  21. package/dist/chunk-6NYR73Y7.js.map +1 -0
  22. package/dist/chunk-APWUCEEB.js +118 -0
  23. package/dist/chunk-APWUCEEB.js.map +1 -0
  24. package/dist/chunk-HNMSWQU7.js +1709 -0
  25. package/dist/chunk-HNMSWQU7.js.map +1 -0
  26. package/dist/chunk-JFYUDERE.js +1412 -0
  27. package/dist/chunk-JFYUDERE.js.map +1 -0
  28. package/dist/chunk-KNUAOILG.js +551 -0
  29. package/dist/chunk-KNUAOILG.js.map +1 -0
  30. package/dist/chunk-KZLVTSBI.js +191 -0
  31. package/dist/chunk-KZLVTSBI.js.map +1 -0
  32. package/dist/chunk-LAKXC4WR.js +2028 -0
  33. package/dist/chunk-LAKXC4WR.js.map +1 -0
  34. package/dist/chunk-NXBAZGNB.js +1107 -0
  35. package/dist/chunk-NXBAZGNB.js.map +1 -0
  36. package/dist/chunk-QRCT5UGZ.js +3286 -0
  37. package/dist/chunk-QRCT5UGZ.js.map +1 -0
  38. package/dist/chunk-S4AJTLLN.js +23 -0
  39. package/dist/chunk-S4AJTLLN.js.map +1 -0
  40. package/dist/chunk-UWBP7B54.js +92 -0
  41. package/dist/chunk-UWBP7B54.js.map +1 -0
  42. package/dist/chunk-XK35ANPJ.js +346 -0
  43. package/dist/chunk-XK35ANPJ.js.map +1 -0
  44. package/dist/chunk-Z7IYVH22.js +4781 -0
  45. package/dist/chunk-Z7IYVH22.js.map +1 -0
  46. package/dist/chunk-ZEBVAIJJ.js +156 -0
  47. package/dist/chunk-ZEBVAIJJ.js.map +1 -0
  48. package/dist/devtools/index.d.ts +536 -0
  49. package/dist/devtools/index.js +15 -0
  50. package/dist/devtools/index.js.map +1 -0
  51. package/dist/environments/index.d.ts +93 -0
  52. package/dist/environments/index.js +382 -0
  53. package/dist/environments/index.js.map +1 -0
  54. package/dist/hotspots/index.d.ts +111 -0
  55. package/dist/hotspots/index.js +285 -0
  56. package/dist/hotspots/index.js.map +1 -0
  57. package/dist/index.d.ts +975 -0
  58. package/dist/index.js +15 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/node/index.cjs +5240 -0
  61. package/dist/node/index.cjs.map +1 -0
  62. package/dist/node/index.d.cts +4927 -0
  63. package/dist/node/index.d.ts +597 -0
  64. package/dist/node/index.js +1328 -0
  65. package/dist/node/index.js.map +1 -0
  66. package/dist/prepare-BtjY4G3q.d.ts +112 -0
  67. package/dist/presets/index.d.ts +562 -0
  68. package/dist/presets/index.js +14 -0
  69. package/dist/presets/index.js.map +1 -0
  70. package/dist/r3f/index.d.ts +159 -0
  71. package/dist/r3f/index.js +592 -0
  72. package/dist/r3f/index.js.map +1 -0
  73. package/dist/room-FS26KAPQ.js +9 -0
  74. package/dist/room-FS26KAPQ.js.map +1 -0
  75. package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
  76. package/dist/session-VCIQEO26.js +9 -0
  77. package/dist/session-VCIQEO26.js.map +1 -0
  78. package/dist/shapes/index.d.ts +222 -0
  79. package/dist/shapes/index.js +836 -0
  80. package/dist/shapes/index.js.map +1 -0
  81. package/dist/testRun-20OARnQr.d.ts +1139 -0
  82. package/dist/timeline-ChwgD7bT.d.ts +470 -0
  83. package/dist/tsl/index.d.ts +165 -0
  84. package/dist/tsl/index.js +310 -0
  85. package/dist/tsl/index.js.map +1 -0
  86. package/package.json +170 -4
@@ -0,0 +1,285 @@
1
+ import { Vector3, Color, Group, Mesh, SphereGeometry, MeshBasicMaterial, BufferGeometry, Line, LineBasicMaterial, Raycaster, Vector2 } from 'three';
2
+
3
+ // src/hotspots/hotspots.ts
4
+ var WORLD = new Vector3();
5
+ var CAMERA = new Vector3();
6
+ var FORWARD = new Vector3();
7
+ var HOTSPOT_COLOR = "#33d1ff";
8
+ var PARTS = /* @__PURE__ */ new WeakMap();
9
+ function hotspotParts(hotspot) {
10
+ const parts = PARTS.get(hotspot);
11
+ if (!parts)
12
+ throw new Error(
13
+ "@oeave/bakery3/hotspots: that hotspot was not made by createHotspot()."
14
+ );
15
+ return parts;
16
+ }
17
+ var HALO_SCALE = 1.7;
18
+ var HALO_PULSE = 0.3;
19
+ var HALO_OPACITY = 0.28;
20
+ var LINE_OPACITY = 0.6;
21
+ var PULSE_HZ = 0.7;
22
+ var WHITE = new Color("#ffffff");
23
+ function toVector3(value) {
24
+ return value instanceof Vector3 ? value.clone() : new Vector3(value[0], value[1], value[2]);
25
+ }
26
+ function vec3Of(vector) {
27
+ return [vector.x, vector.y, vector.z];
28
+ }
29
+ function createHotspot(options) {
30
+ const {
31
+ id,
32
+ label,
33
+ parent,
34
+ color = HOTSPOT_COLOR,
35
+ sizing = "screen",
36
+ size = sizing === "screen" ? 0.012 : 0.03,
37
+ pulse = true
38
+ } = options;
39
+ const radius = sizing === "screen" ? 1 : size;
40
+ const rig = new Group();
41
+ rig.name = "rig";
42
+ const position = toVector3(options.position);
43
+ const anchor = options.anchor ? toVector3(options.anchor) : void 0;
44
+ const object = new Group();
45
+ object.name = `bakery3-hotspot-${id}`;
46
+ object.userData.bakery3 = { exclude: true };
47
+ object.position.copy(position);
48
+ const orb = new Mesh(
49
+ new SphereGeometry(radius, 24, 16),
50
+ new MeshBasicMaterial({ color, toneMapped: false, depthTest: true })
51
+ );
52
+ orb.name = "orb";
53
+ rig.add(orb);
54
+ const halo = new Mesh(
55
+ new SphereGeometry(radius, 24, 16),
56
+ new MeshBasicMaterial({
57
+ color,
58
+ toneMapped: false,
59
+ transparent: true,
60
+ opacity: HALO_OPACITY,
61
+ depthWrite: false
62
+ })
63
+ );
64
+ halo.name = "halo";
65
+ halo.scale.setScalar(HALO_SCALE);
66
+ halo.renderOrder = 1;
67
+ rig.add(halo);
68
+ if (sizing === "screen") rig.scale.setScalar(size);
69
+ object.add(rig);
70
+ let line;
71
+ if (anchor) {
72
+ const geometry = new BufferGeometry().setFromPoints([
73
+ anchor.clone().sub(position),
74
+ new Vector3(0, 0, 0)
75
+ ]);
76
+ line = new Line(
77
+ geometry,
78
+ new LineBasicMaterial({
79
+ color,
80
+ toneMapped: false,
81
+ transparent: true,
82
+ opacity: LINE_OPACITY,
83
+ depthWrite: false
84
+ })
85
+ );
86
+ line.name = "tether";
87
+ object.add(line);
88
+ }
89
+ parent?.add(object);
90
+ let selected = false;
91
+ let hovered = false;
92
+ let disposed = false;
93
+ const applyLook = () => {
94
+ const emphasis = selected ? 1.25 : hovered ? 1.1 : 1;
95
+ orb.scale.setScalar(emphasis);
96
+ orb.material.color.set(color);
97
+ if (selected) orb.material.color.lerp(WHITE, 0.5);
98
+ halo.material.opacity = selected ? HALO_OPACITY * 0.6 : hovered ? HALO_OPACITY * 1.6 : HALO_OPACITY;
99
+ if (line) line.material.opacity = selected || hovered ? 1 : LINE_OPACITY;
100
+ };
101
+ const hotspot = {
102
+ id,
103
+ label,
104
+ object,
105
+ get selected() {
106
+ return selected;
107
+ },
108
+ get hovered() {
109
+ return hovered;
110
+ },
111
+ setSelected(on) {
112
+ if (selected === on) return;
113
+ selected = on;
114
+ applyLook();
115
+ },
116
+ fit(camera) {
117
+ if (disposed || sizing !== "screen") return;
118
+ const perspective = camera.isPerspectiveCamera === true;
119
+ const ortho = camera.isOrthographicCamera === true;
120
+ if (!perspective && !ortho) return;
121
+ object.getWorldPosition(WORLD);
122
+ camera.getWorldPosition(CAMERA);
123
+ let visibleHeight;
124
+ if (perspective) {
125
+ const cam = camera;
126
+ camera.getWorldDirection(FORWARD);
127
+ const depth = Math.max(1e-3, WORLD.sub(CAMERA).dot(FORWARD));
128
+ visibleHeight = 2 * depth * Math.tan(cam.fov * Math.PI / 360);
129
+ } else {
130
+ const cam = camera;
131
+ visibleHeight = (cam.top - cam.bottom) / cam.zoom;
132
+ }
133
+ rig.scale.setScalar(Math.max(1e-6, size * visibleHeight));
134
+ },
135
+ update(seconds) {
136
+ if (disposed || !pulse) return;
137
+ const phase = 0.5 + 0.5 * Math.sin(seconds * Math.PI * 2 * PULSE_HZ);
138
+ const rest = selected ? HALO_SCALE * 0.8 : HALO_SCALE;
139
+ halo.scale.setScalar(rest + HALO_PULSE * phase * (selected ? 0.5 : 1));
140
+ },
141
+ dispose() {
142
+ if (disposed) return;
143
+ disposed = true;
144
+ object.removeFromParent();
145
+ orb.geometry.dispose();
146
+ orb.material.dispose();
147
+ halo.geometry.dispose();
148
+ halo.material.dispose();
149
+ line?.geometry.dispose();
150
+ line?.material.dispose();
151
+ }
152
+ };
153
+ PARTS.set(hotspot, {
154
+ orb,
155
+ halo,
156
+ rig,
157
+ line,
158
+ setHovered(on) {
159
+ if (hovered === on) return;
160
+ hovered = on;
161
+ applyLook();
162
+ }
163
+ });
164
+ return hotspot;
165
+ }
166
+ function hotspotSpecOf(hotspot) {
167
+ const { object } = hotspot;
168
+ object.updateWorldMatrix(true, false);
169
+ const spec = {
170
+ id: hotspot.id,
171
+ position: vec3Of(object.getWorldPosition(new Vector3()))
172
+ };
173
+ if (hotspot.label !== void 0) spec.label = hotspot.label;
174
+ const { line } = hotspotParts(hotspot);
175
+ if (line) {
176
+ const local = new Vector3().fromBufferAttribute(
177
+ line.geometry.getAttribute("position"),
178
+ 0
179
+ );
180
+ spec.anchor = vec3Of(object.localToWorld(local));
181
+ }
182
+ return spec;
183
+ }
184
+ var CLICK_SLOP_PX = 6;
185
+ function attachHotspots(options) {
186
+ const { hotspots, camera, domElement, onSelect, onHover } = options;
187
+ const raycaster = new Raycaster();
188
+ const pointer = new Vector2();
189
+ const targets = /* @__PURE__ */ new Map();
190
+ for (const hotspot of hotspots) {
191
+ const { orb, halo } = hotspotParts(hotspot);
192
+ targets.set(orb, hotspot);
193
+ targets.set(halo, hotspot);
194
+ }
195
+ let selected = null;
196
+ let hovered = null;
197
+ let downAt = null;
198
+ let previousCursor = null;
199
+ const pick = (event) => {
200
+ const rect = domElement.getBoundingClientRect();
201
+ if (rect.width === 0 || rect.height === 0) return null;
202
+ pointer.set(
203
+ (event.clientX - rect.left) / rect.width * 2 - 1,
204
+ -((event.clientY - rect.top) / rect.height) * 2 + 1
205
+ );
206
+ raycaster.setFromCamera(pointer, camera);
207
+ const hits = raycaster.intersectObjects([...targets.keys()], false);
208
+ for (const hit of hits) {
209
+ const hotspot = targets.get(hit.object);
210
+ if (hotspot && hotspot.object.visible) return hotspot;
211
+ }
212
+ return null;
213
+ };
214
+ const setHovered = (next) => {
215
+ if (next === hovered) return;
216
+ if (hovered) hotspotParts(hovered).setHovered(false);
217
+ hovered = next;
218
+ if (hovered) hotspotParts(hovered).setHovered(true);
219
+ if (hovered) {
220
+ if (previousCursor === null) previousCursor = domElement.style.cursor;
221
+ domElement.style.cursor = "pointer";
222
+ } else if (previousCursor !== null) {
223
+ domElement.style.cursor = previousCursor;
224
+ previousCursor = null;
225
+ }
226
+ onHover?.(hovered);
227
+ };
228
+ const select = (next) => {
229
+ if (next === selected) return;
230
+ selected?.setSelected(false);
231
+ selected = next;
232
+ selected?.setSelected(true);
233
+ onSelect?.(selected);
234
+ };
235
+ const onPointerMove = (event) => setHovered(pick(event));
236
+ const onPointerLeave = () => setHovered(null);
237
+ const onPointerDown = (event) => {
238
+ if (event.button !== 0) return;
239
+ downAt = { x: event.clientX, y: event.clientY };
240
+ };
241
+ const onPointerUp = (event) => {
242
+ if (!downAt || event.button !== 0) return;
243
+ const moved = Math.hypot(
244
+ event.clientX - downAt.x,
245
+ event.clientY - downAt.y
246
+ );
247
+ downAt = null;
248
+ if (moved > CLICK_SLOP_PX) return;
249
+ const hit = pick(event);
250
+ select(hit === selected ? null : hit);
251
+ };
252
+ const listens = typeof domElement?.addEventListener === "function";
253
+ if (listens) {
254
+ domElement.addEventListener("pointermove", onPointerMove);
255
+ domElement.addEventListener("pointerleave", onPointerLeave);
256
+ domElement.addEventListener("pointerdown", onPointerDown);
257
+ domElement.addEventListener("pointerup", onPointerUp);
258
+ }
259
+ return {
260
+ get selected() {
261
+ return selected;
262
+ },
263
+ select,
264
+ update(seconds) {
265
+ for (const hotspot of hotspots) {
266
+ hotspot.fit(camera);
267
+ hotspot.update(seconds);
268
+ }
269
+ },
270
+ dispose() {
271
+ if (listens) {
272
+ domElement.removeEventListener("pointermove", onPointerMove);
273
+ domElement.removeEventListener("pointerleave", onPointerLeave);
274
+ domElement.removeEventListener("pointerdown", onPointerDown);
275
+ domElement.removeEventListener("pointerup", onPointerUp);
276
+ }
277
+ setHovered(null);
278
+ hovered = null;
279
+ }
280
+ };
281
+ }
282
+
283
+ export { attachHotspots, createHotspot, hotspotSpecOf };
284
+ //# sourceMappingURL=index.js.map
285
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hotspots/hotspots.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The implementation behind `@oeave/bakery3/hotspots`. `index.ts` lists what\n * is public; the orb's parts and the brand color are here for the SDK's own\n * pointer handling and the monorepo's tools (`internal.ts`).\n */\n\nimport {\n BufferGeometry,\n Color,\n Group,\n Line,\n LineBasicMaterial,\n Mesh,\n MeshBasicMaterial,\n Raycaster,\n SphereGeometry,\n Vector2,\n Vector3,\n type Camera,\n type Object3D,\n type PerspectiveCamera,\n type OrthographicCamera,\n} from 'three';\n\nconst WORLD = new Vector3();\nconst CAMERA = new Vector3();\nconst FORWARD = new Vector3();\n\n/** `[x, y, z]`, in meters: the protocol's own vector shape. */\nexport type Vec3 = [number, number, number];\n\n/** The brand cyan; what an orb is unless told otherwise. */\nexport const HOTSPOT_COLOR = '#33d1ff';\n\n/** The orb's pieces: what the pointer handling raycasts and restyles. */\nexport type HotspotParts = {\n /** The solid core; what `attachHotspots` raycasts against. */\n readonly orb: Mesh<SphereGeometry, MeshBasicMaterial>;\n /** The translucent shell around the core, the part that pulses. */\n readonly halo: Mesh<SphereGeometry, MeshBasicMaterial>;\n /** Holds the orb and halo; `fit()` scales this, never the group. */\n readonly rig: Group;\n /** Present only when anchored: the tether from the surface point to the orb. */\n readonly line: Line<BufferGeometry, LineBasicMaterial> | undefined;\n /** Set by `attachHotspots` as the pointer moves on and off. */\n setHovered(on: boolean): void;\n};\n\nconst PARTS = new WeakMap<Hotspot, HotspotParts>();\n\n/** The parts of a hotspot `createHotspot` made. */\nexport function hotspotParts(hotspot: Hotspot): HotspotParts {\n const parts = PARTS.get(hotspot);\n if (!parts)\n throw new Error(\n '@oeave/bakery3/hotspots: that hotspot was not made by createHotspot().',\n );\n return parts;\n}\n\nexport type HotspotOptions = {\n /** Stable identity: it names the group and travels with `hotspotSpecOf`. */\n id: string;\n /** A caption for the app to show; the orb itself draws no text. */\n label?: string;\n /** Where the orb is, in the parent's space (world space without a parent). */\n position: Vec3 | Vector3;\n /**\n * A surface point on the model, in the same space as `position`. When given\n * the hotspot is anchored: a thin line is drawn from here to the orb.\n */\n anchor?: Vec3 | Vector3;\n /**\n * Attach to this object so the hotspot follows it. Without one the caller\n * adds `hotspot.object` to the scene themselves.\n */\n parent?: Object3D;\n /** CSS color of the orb, halo and line. Default the brand cyan, `#33d1ff`. */\n color?: string;\n /**\n * How the orb is sized. `screen` (default) keeps it the same size on screen\n * wherever it is: `size` is then a fraction of the viewport height, and\n * `attachHotspots` (or `fit()`) re-fits it every frame from the camera.\n * `world` is a fixed radius in meters, which shrinks with distance.\n */\n sizing?: 'screen' | 'world';\n /** Orb radius: a fraction of viewport height (`screen`, default 0.012) or meters (`world`, default 0.03). */\n size?: number;\n /** Whether the halo breathes. Default true. */\n pulse?: boolean;\n};\n\nexport type Hotspot = {\n readonly id: string;\n readonly label: string | undefined;\n /**\n * The group holding everything (the orb, and the tether when anchored),\n * placed at the orb; excluded from capture, named `bakery3-hotspot-<id>`.\n */\n readonly object: Group;\n /** Whether `setSelected(true)` is in effect. */\n readonly selected: boolean;\n /** Whether the cursor is over it; set by `attachHotspots`. */\n readonly hovered: boolean;\n setSelected(on: boolean): void;\n /** Advance the pulse; `seconds` is any monotonic clock. */\n update(seconds: number): void;\n /**\n * Scale the orb so it projects to its `size` for this camera. Called by\n * `attachHotspots.update()`; a host without pointer handling calls it\n * itself once per frame. No-op for `sizing: 'world'`.\n */\n fit(camera: Camera): void;\n /** Detach from its parent and free every geometry and material. */\n dispose(): void;\n};\n\n/** A hotspot as data, in world space: what to keep with a capture. */\nexport type HotspotSpec = {\n id: string;\n label?: string;\n position: Vec3;\n anchor?: Vec3;\n};\n\n// Rest, hovered and selected looks, as the ratios a 0.03 m orb was tuned at.\nconst HALO_SCALE = 1.7;\nconst HALO_PULSE = 0.3;\nconst HALO_OPACITY = 0.28;\nconst LINE_OPACITY = 0.6;\nconst PULSE_HZ = 0.7;\nconst WHITE = new Color('#ffffff');\n\nfunction toVector3(value: Vec3 | Vector3): Vector3 {\n return value instanceof Vector3\n ? value.clone()\n : new Vector3(value[0], value[1], value[2]);\n}\n\nfunction vec3Of(vector: Vector3): Vec3 {\n return [vector.x, vector.y, vector.z];\n}\n\nexport function createHotspot(options: HotspotOptions): Hotspot {\n const {\n id,\n label,\n parent,\n color = HOTSPOT_COLOR,\n sizing = 'screen',\n size = sizing === 'screen' ? 0.012 : 0.03,\n pulse = true,\n } = options;\n // Screen-sized orbs are built at unit radius and scaled by `fit()`; the\n // scale lives on a rig of its own so the tether, which is drawn in the\n // group's space, never scales with it.\n const radius = sizing === 'screen' ? 1 : size;\n const rig = new Group();\n rig.name = 'rig';\n const position = toVector3(options.position);\n const anchor = options.anchor ? toVector3(options.anchor) : undefined;\n\n const object = new Group();\n object.name = `bakery3-hotspot-${id}`;\n // The exclusion the capture honors. It covers the whole subtree, so the\n // line and halo need no mark of their own.\n object.userData.bakery3 = { exclude: true };\n object.position.copy(position);\n\n // Unlit on purpose: an orb that picks up the scene's lighting turns into a\n // gray ball in shadow and a white blob in sun. `toneMapped: false` keeps the\n // brand cyan the brand cyan under ACES too.\n const orb = new Mesh(\n new SphereGeometry(radius, 24, 16),\n new MeshBasicMaterial({ color, toneMapped: false, depthTest: true }),\n );\n orb.name = 'orb';\n rig.add(orb);\n\n const halo = new Mesh(\n new SphereGeometry(radius, 24, 16),\n new MeshBasicMaterial({\n color,\n toneMapped: false,\n transparent: true,\n opacity: HALO_OPACITY,\n depthWrite: false,\n }),\n );\n halo.name = 'halo';\n halo.scale.setScalar(HALO_SCALE);\n // After the opaque core so its translucency composites over it.\n halo.renderOrder = 1;\n rig.add(halo);\n if (sizing === 'screen') rig.scale.setScalar(size); // a sane size before the first fit\n object.add(rig);\n\n let line: Line<BufferGeometry, LineBasicMaterial> | undefined;\n if (anchor) {\n // The group sits at the orb, so the anchor is expressed relative to it.\n // The group is never rotated or scaled by this module, which is what\n // makes a plain subtraction the right transform.\n const geometry = new BufferGeometry().setFromPoints([\n anchor.clone().sub(position),\n new Vector3(0, 0, 0),\n ]);\n line = new Line(\n geometry,\n new LineBasicMaterial({\n color,\n toneMapped: false,\n transparent: true,\n opacity: LINE_OPACITY,\n depthWrite: false,\n }),\n );\n line.name = 'tether';\n object.add(line);\n }\n\n parent?.add(object);\n\n let selected = false;\n let hovered = false;\n let disposed = false;\n\n const applyLook = (): void => {\n // Selected: a brighter, larger core with the halo drawn back, so the orb\n // becomes a filled dot. Hovered: just a touch bigger, the halo stronger.\n const emphasis = selected ? 1.25 : hovered ? 1.1 : 1;\n orb.scale.setScalar(emphasis);\n orb.material.color.set(color);\n if (selected) orb.material.color.lerp(WHITE, 0.5);\n halo.material.opacity = selected\n ? HALO_OPACITY * 0.6\n : hovered\n ? HALO_OPACITY * 1.6\n : HALO_OPACITY;\n if (line) line.material.opacity = selected || hovered ? 1 : LINE_OPACITY;\n };\n\n const hotspot: Hotspot = {\n id,\n label,\n object,\n get selected() {\n return selected;\n },\n get hovered() {\n return hovered;\n },\n setSelected(on) {\n if (selected === on) return;\n selected = on;\n applyLook();\n },\n fit(camera) {\n if (disposed || sizing !== 'screen') return;\n const perspective =\n (camera as PerspectiveCamera).isPerspectiveCamera === true;\n const ortho =\n (camera as OrthographicCamera).isOrthographicCamera === true;\n if (!perspective && !ortho) return;\n object.getWorldPosition(WORLD);\n camera.getWorldPosition(CAMERA);\n let visibleHeight: number;\n if (perspective) {\n const cam = camera as PerspectiveCamera;\n // Depth along the view axis, not the straight-line distance: an orb\n // at the frame's edge is as far \"into\" the picture as one dead center.\n camera.getWorldDirection(FORWARD);\n const depth = Math.max(1e-3, WORLD.sub(CAMERA).dot(FORWARD));\n visibleHeight = 2 * depth * Math.tan((cam.fov * Math.PI) / 360);\n } else {\n const cam = camera as OrthographicCamera;\n visibleHeight = (cam.top - cam.bottom) / cam.zoom;\n }\n rig.scale.setScalar(Math.max(1e-6, size * visibleHeight));\n },\n update(seconds) {\n if (disposed || !pulse) return;\n // A slow breath: never so fast it reads as an alarm.\n const phase = 0.5 + 0.5 * Math.sin(seconds * Math.PI * 2 * PULSE_HZ);\n const rest = selected ? HALO_SCALE * 0.8 : HALO_SCALE;\n halo.scale.setScalar(rest + HALO_PULSE * phase * (selected ? 0.5 : 1));\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n object.removeFromParent();\n orb.geometry.dispose();\n orb.material.dispose();\n halo.geometry.dispose();\n halo.material.dispose();\n line?.geometry.dispose();\n line?.material.dispose();\n },\n };\n PARTS.set(hotspot, {\n orb,\n halo,\n rig,\n line,\n setHovered(on) {\n if (hovered === on) return;\n hovered = on;\n applyLook();\n },\n });\n return hotspot;\n}\n\n/**\n * A hotspot as plain data, in world space, ready for `productState`.\n *\n * Reads through the parents so a hotspot riding on a product that has been\n * moved reports where it actually is, not where it was authored.\n */\nexport function hotspotSpecOf(hotspot: Hotspot): HotspotSpec {\n const { object } = hotspot;\n object.updateWorldMatrix(true, false);\n const spec: HotspotSpec = {\n id: hotspot.id,\n position: vec3Of(object.getWorldPosition(new Vector3())),\n };\n if (hotspot.label !== undefined) spec.label = hotspot.label;\n const { line } = hotspotParts(hotspot);\n if (line) {\n const local = new Vector3().fromBufferAttribute(\n line.geometry.getAttribute('position'),\n 0,\n );\n spec.anchor = vec3Of(object.localToWorld(local));\n }\n return spec;\n}\n\nexport type AttachHotspotsOptions = {\n hotspots: Hotspot[];\n camera: Camera;\n /** The renderer's canvas, or whatever element receives the pointer. */\n domElement: HTMLElement;\n /** Selection changed: the hotspot now selected, or null when cleared. */\n onSelect?(hotspot: Hotspot | null): void;\n /** The pointer moved onto (or off) a hotspot. */\n onHover?(hotspot: Hotspot | null): void;\n};\n\nexport type AttachedHotspots = {\n /** Advance every hotspot's pulse. Call once per frame. */\n update(seconds: number): void;\n /** The hotspot currently selected, if any. */\n readonly selected: Hotspot | null;\n /** Select (or clear) programmatically; fires `onSelect` like a click would. */\n select(hotspot: Hotspot | null): void;\n /** Remove the listeners. Does not dispose the hotspots. */\n dispose(): void;\n};\n\n/** How far a pointer may travel between down and up and still be a click, in\n * CSS pixels: an orbit drag that happens to end on an orb is not a pick. */\nconst CLICK_SLOP_PX = 6;\n\n/**\n * Wire hover and click for a set of hotspots on one canvas.\n *\n * Raycasts the orbs (core and halo, so the target is a little generous) from\n * the pointer through `camera`; sets `cursor: pointer` over one; toggles\n * selection on click, one at a time. Nothing here is a render loop: call\n * `update()` from yours.\n */\nexport function attachHotspots(\n options: AttachHotspotsOptions,\n): AttachedHotspots {\n const { hotspots, camera, domElement, onSelect, onHover } = options;\n const raycaster = new Raycaster();\n const pointer = new Vector2();\n const targets = new Map<Object3D, Hotspot>();\n for (const hotspot of hotspots) {\n const { orb, halo } = hotspotParts(hotspot);\n targets.set(orb, hotspot);\n targets.set(halo, hotspot);\n }\n\n let selected: Hotspot | null = null;\n let hovered: Hotspot | null = null;\n let downAt: { x: number; y: number } | null = null;\n let previousCursor: string | null = null;\n\n const pick = (event: PointerEvent): Hotspot | null => {\n const rect = domElement.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return null;\n pointer.set(\n ((event.clientX - rect.left) / rect.width) * 2 - 1,\n -((event.clientY - rect.top) / rect.height) * 2 + 1,\n );\n raycaster.setFromCamera(pointer, camera);\n const hits = raycaster.intersectObjects([...targets.keys()], false);\n for (const hit of hits) {\n const hotspot = targets.get(hit.object);\n if (hotspot && hotspot.object.visible) return hotspot;\n }\n return null;\n };\n\n const setHovered = (next: Hotspot | null): void => {\n if (next === hovered) return;\n if (hovered) hotspotParts(hovered).setHovered(false);\n hovered = next;\n if (hovered) hotspotParts(hovered).setHovered(true);\n if (hovered) {\n if (previousCursor === null) previousCursor = domElement.style.cursor;\n domElement.style.cursor = 'pointer';\n } else if (previousCursor !== null) {\n domElement.style.cursor = previousCursor;\n previousCursor = null;\n }\n onHover?.(hovered);\n };\n\n const select = (next: Hotspot | null): void => {\n if (next === selected) return;\n selected?.setSelected(false);\n selected = next;\n selected?.setSelected(true);\n onSelect?.(selected);\n };\n\n const onPointerMove = (event: PointerEvent): void => setHovered(pick(event));\n const onPointerLeave = (): void => setHovered(null);\n const onPointerDown = (event: PointerEvent): void => {\n if (event.button !== 0) return;\n downAt = { x: event.clientX, y: event.clientY };\n };\n const onPointerUp = (event: PointerEvent): void => {\n if (!downAt || event.button !== 0) return;\n const moved = Math.hypot(\n event.clientX - downAt.x,\n event.clientY - downAt.y,\n );\n downAt = null;\n if (moved > CLICK_SLOP_PX) return;\n const hit = pick(event);\n // Clicking the selected orb clears it; clicking empty space clears too.\n select(hit === selected ? null : hit);\n };\n\n // Guarded so a hotspot set can be built where there is no DOM (a test, a\n // server) and simply never receive a pointer.\n const listens = typeof domElement?.addEventListener === 'function';\n if (listens) {\n domElement.addEventListener('pointermove', onPointerMove);\n domElement.addEventListener('pointerleave', onPointerLeave);\n domElement.addEventListener('pointerdown', onPointerDown);\n domElement.addEventListener('pointerup', onPointerUp);\n }\n\n return {\n get selected() {\n return selected;\n },\n select,\n update(seconds) {\n for (const hotspot of hotspots) {\n hotspot.fit(camera);\n hotspot.update(seconds);\n }\n },\n dispose() {\n if (listens) {\n domElement.removeEventListener('pointermove', onPointerMove);\n domElement.removeEventListener('pointerleave', onPointerLeave);\n domElement.removeEventListener('pointerdown', onPointerDown);\n domElement.removeEventListener('pointerup', onPointerUp);\n }\n setHovered(null);\n hovered = null;\n },\n };\n}\n"]}