@oeave/bakery3 0.2.0 → 0.3.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 (46) hide show
  1. package/dist/bake/index.js +3 -3
  2. package/dist/catalog/index.js +1 -1
  3. package/dist/{chunk-KNUAOILG.js → chunk-3T3FJOSD.js} +4 -4
  4. package/dist/{chunk-KNUAOILG.js.map → chunk-3T3FJOSD.js.map} +1 -1
  5. package/dist/{chunk-6FYI6AJM.js → chunk-6FAMQZXB.js} +7 -7
  6. package/dist/{chunk-6FYI6AJM.js.map → chunk-6FAMQZXB.js.map} +1 -1
  7. package/dist/{chunk-QRCT5UGZ.js → chunk-CSSJRP5W.js} +9 -6
  8. package/dist/chunk-CSSJRP5W.js.map +1 -0
  9. package/dist/{chunk-APWUCEEB.js → chunk-EN4ROBYQ.js} +3 -3
  10. package/dist/{chunk-APWUCEEB.js.map → chunk-EN4ROBYQ.js.map} +1 -1
  11. package/dist/{chunk-JFYUDERE.js → chunk-NEK5TBEH.js} +2 -2
  12. package/dist/chunk-NEK5TBEH.js.map +1 -0
  13. package/dist/chunk-OPM2L63B.js +253 -0
  14. package/dist/chunk-OPM2L63B.js.map +1 -0
  15. package/dist/{chunk-UWBP7B54.js → chunk-R5IUPOWI.js} +3 -3
  16. package/dist/{chunk-UWBP7B54.js.map → chunk-R5IUPOWI.js.map} +1 -1
  17. package/dist/{chunk-HNMSWQU7.js → chunk-STRK4SZN.js} +3 -3
  18. package/dist/{chunk-HNMSWQU7.js.map → chunk-STRK4SZN.js.map} +1 -1
  19. package/dist/{chunk-Z7IYVH22.js → chunk-WLEFNUOH.js} +30 -279
  20. package/dist/chunk-WLEFNUOH.js.map +1 -0
  21. package/dist/{chunk-NXBAZGNB.js → chunk-X6GW43EX.js} +3 -3
  22. package/dist/{chunk-NXBAZGNB.js.map → chunk-X6GW43EX.js.map} +1 -1
  23. package/dist/devtools/index.js +7 -6
  24. package/dist/hotspots/index.d.ts +237 -31
  25. package/dist/hotspots/index.js +1013 -118
  26. package/dist/hotspots/index.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +7 -6
  29. package/dist/node/index.cjs +1 -1
  30. package/dist/node/index.cjs.map +1 -1
  31. package/dist/node/index.js +2 -2
  32. package/dist/presets/index.js +2 -2
  33. package/dist/r3f/index.js +9 -8
  34. package/dist/r3f/index.js.map +1 -1
  35. package/dist/{room-FS26KAPQ.js → room-BM7Z7NKG.js} +4 -4
  36. package/dist/{room-FS26KAPQ.js.map → room-BM7Z7NKG.js.map} +1 -1
  37. package/dist/session-NPVBESWA.js +9 -0
  38. package/dist/{session-VCIQEO26.js.map → session-NPVBESWA.js.map} +1 -1
  39. package/dist/tsl/index.d.ts +14 -1
  40. package/dist/tsl/index.js +8 -2
  41. package/dist/tsl/index.js.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/chunk-JFYUDERE.js.map +0 -1
  44. package/dist/chunk-QRCT5UGZ.js.map +0 -1
  45. package/dist/chunk-Z7IYVH22.js.map +0 -1
  46. package/dist/session-VCIQEO26.js +0 -9
@@ -1,11 +1,125 @@
1
- import { Vector3, Color, Group, Mesh, SphereGeometry, MeshBasicMaterial, BufferGeometry, Line, LineBasicMaterial, Raycaster, Vector2 } from 'three';
1
+ import { OBJECT_PROPERTIES } from '../chunk-OPM2L63B.js';
2
+ import { Vector3, Quaternion, Euler, Group, Color, Mesh, TorusGeometry, CylinderGeometry, MeshPhysicalMaterial, PointLight, Matrix3, Matrix4, Raycaster, Vector2, SphereGeometry, MeshStandardMaterial, Shape, ShapeGeometry, DataTexture, RGBAFormat, UnsignedByteType, SRGBColorSpace, LinearFilter, LinearMipmapLinearFilter } from 'three';
3
+
4
+ var LABEL_FONT = '600 40px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
5
+ function context2d(width, height) {
6
+ const usable = (ctx) => !!ctx && typeof ctx.measureText === "function" && typeof ctx.getImageData === "function";
7
+ try {
8
+ if (typeof OffscreenCanvas !== "undefined") {
9
+ const ctx = new OffscreenCanvas(width, height).getContext("2d");
10
+ if (usable(ctx)) return ctx;
11
+ }
12
+ if (typeof document !== "undefined") {
13
+ const canvas = document.createElement("canvas");
14
+ if (typeof canvas?.getContext !== "function") return null;
15
+ canvas.width = width;
16
+ canvas.height = height;
17
+ const ctx = canvas.getContext("2d");
18
+ if (usable(ctx)) return ctx;
19
+ }
20
+ } catch {
21
+ }
22
+ return null;
23
+ }
24
+ function pixelSize(font) {
25
+ const match = /(\d+(?:\.\d+)?)px/.exec(font);
26
+ return match ? Number(match[1]) : 40;
27
+ }
28
+ function texture(data, width, height) {
29
+ const map = new DataTexture(
30
+ data,
31
+ width,
32
+ height,
33
+ RGBAFormat,
34
+ UnsignedByteType
35
+ );
36
+ map.colorSpace = SRGBColorSpace;
37
+ map.magFilter = LinearFilter;
38
+ map.minFilter = LinearMipmapLinearFilter;
39
+ map.generateMipmaps = true;
40
+ map.anisotropy = 4;
41
+ map.needsUpdate = true;
42
+ return map;
43
+ }
44
+ function plain(look) {
45
+ const hex = new Color(look.background).getHex();
46
+ const rgba = [hex >> 16 & 255, hex >> 8 & 255, hex & 255];
47
+ const data = new Uint8Array(12);
48
+ for (let i = 0; i < 3; i++)
49
+ data.set([rgba[0], rgba[1], rgba[2], 255], i * 4);
50
+ return { texture: texture(data, 3, 1), aspect: 3 };
51
+ }
52
+ function rasterizeLabel(look) {
53
+ const size = pixelSize(look.font);
54
+ const height = Math.max(8, Math.round(size * 1.7));
55
+ const pad = Math.round(size * 0.62);
56
+ const probe = context2d(1, 1);
57
+ if (!probe) return plain(look);
58
+ probe.font = look.font;
59
+ const width = Math.max(
60
+ height,
61
+ Math.ceil(probe.measureText(look.text).width + pad * 2)
62
+ );
63
+ const ctx = context2d(width, height);
64
+ if (!ctx) return plain(look);
65
+ ctx.fillStyle = look.background;
66
+ ctx.fillRect(0, 0, width, height);
67
+ ctx.font = look.font;
68
+ ctx.textBaseline = "middle";
69
+ ctx.fillStyle = look.color;
70
+ ctx.fillText(look.text, pad, height / 2 + size * 0.04);
71
+ const pixels = ctx.getImageData(0, 0, width, height).data;
72
+ const data = new Uint8Array(width * height * 4);
73
+ const row = width * 4;
74
+ for (let y = 0; y < height; y++)
75
+ data.set(
76
+ pixels.subarray((height - 1 - y) * row, (height - y) * row),
77
+ y * row
78
+ );
79
+ return { texture: texture(data, width, height), aspect: width / height };
80
+ }
81
+ function pillGeometry(aspect) {
82
+ const width = Math.max(1, aspect);
83
+ const r = 0.5;
84
+ const shape = new Shape();
85
+ shape.moveTo(r, -0.5);
86
+ shape.lineTo(width - r, -0.5);
87
+ shape.absarc(width - r, 0, r, -Math.PI / 2, Math.PI / 2, false);
88
+ shape.lineTo(r, 0.5);
89
+ shape.absarc(r, 0, r, Math.PI / 2, 3 * Math.PI / 2, false);
90
+ const geometry = new ShapeGeometry(shape, 16);
91
+ const position = geometry.getAttribute("position");
92
+ const uv = geometry.getAttribute("uv");
93
+ for (let i = 0; i < position.count; i++)
94
+ uv.setXY(i, position.getX(i) / width, position.getY(i) + 0.5);
95
+ uv.needsUpdate = true;
96
+ return geometry;
97
+ }
98
+ function labelSideFor(current, x, reach) {
99
+ const margin = 0.04;
100
+ const fitsRight = x + reach < 1 - margin;
101
+ const fitsLeft = x - reach > -1 + margin;
102
+ if (!fitsRight && !fitsLeft)
103
+ return current === "right" ? x > 0.1 ? "left" : "right" : x < -0.1 ? "right" : "left";
104
+ if (current === "right") return fitsRight ? "right" : "left";
105
+ return x + reach < 1 - margin - 0.12 || !fitsLeft ? "right" : "left";
106
+ }
2
107
 
3
108
  // src/hotspots/hotspots.ts
4
109
  var WORLD = new Vector3();
5
110
  var CAMERA = new Vector3();
6
111
  var FORWARD = new Vector3();
112
+ var SCALE = new Vector3();
113
+ var RIGHT = new Vector3();
114
+ var EDGE = new Vector3();
115
+ var ORIENT = new Quaternion();
116
+ var VIEW = new Quaternion();
117
+ var EULER = new Euler();
118
+ var UP = new Vector3(0, 1, 0);
119
+ var FRONT = new Vector3(0, 0, 1);
7
120
  var HOTSPOT_COLOR = "#33d1ff";
8
121
  var PARTS = /* @__PURE__ */ new WeakMap();
122
+ var INTERNALS = /* @__PURE__ */ new WeakMap();
9
123
  function hotspotParts(hotspot) {
10
124
  const parts = PARTS.get(hotspot);
11
125
  if (!parts)
@@ -17,169 +131,944 @@ function hotspotParts(hotspot) {
17
131
  var HALO_SCALE = 1.7;
18
132
  var HALO_PULSE = 0.3;
19
133
  var HALO_OPACITY = 0.28;
20
- var LINE_OPACITY = 0.6;
134
+ var RING_RADIUS = 1.35;
135
+ var RING_TUBE = 0.11;
136
+ var GLASS_CORE = 0.42;
137
+ var RING_CORE = 0.5;
138
+ var TETHER_RADIUS = 0.09;
139
+ var DOT_RADIUS = 0.32;
21
140
  var PULSE_HZ = 0.7;
22
- var WHITE = new Color("#ffffff");
23
- function toVector3(value) {
141
+ var LABEL_HEIGHT = 2.2;
142
+ var LABEL_SLIDE = 0.6;
143
+ var LABEL_LIFT = 0.4;
144
+ var LABEL_GLOW = 1.3;
145
+ var REACH = {
146
+ orb: 2,
147
+ ring: 2,
148
+ glass: 1.15,
149
+ solid: 1.45
150
+ };
151
+ var CLOSED_GLOW = 1 / 3;
152
+ var FLOOR = 1e-3;
153
+ var LIGHT_FLOOR = 1e-6;
154
+ var INTENSITY = {
155
+ orb: 3,
156
+ ring: 3,
157
+ glass: 6,
158
+ solid: 0.2
159
+ };
160
+ var clamp01 = (x) => x <= 0 ? 0 : x >= 1 ? 1 : x;
161
+ var smooth = (a, b, x) => {
162
+ const t = clamp01((x - a) / (b - a));
163
+ return t * t * (3 - 2 * t);
164
+ };
165
+ var easeOutCubic = (t) => 1 - (1 - clamp01(t)) ** 3;
166
+ var easeInOutCubic = (t) => {
167
+ const x = clamp01(t);
168
+ return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
169
+ };
170
+ var easeOutBack = (t) => {
171
+ const x = clamp01(t);
172
+ const c1 = 1.4;
173
+ const c3 = c1 + 1;
174
+ return 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2;
175
+ };
176
+ function poseOf(motion, seconds, anchored, pulseHz) {
177
+ const p = motion.presence;
178
+ const head = anchored ? easeOutBack((p - 0.45) / 0.55) : easeOutBack(p);
179
+ return {
180
+ dot: anchored ? smooth(0, 0.3, p) : 0,
181
+ extend: anchored ? easeOutCubic((p - 0.15) / 0.6) : 1,
182
+ head: Math.max(0, head),
183
+ open: easeInOutCubic(motion.open) * smooth(0.6, 1, p),
184
+ hover: smooth(0, 1, motion.hover),
185
+ label: easeOutCubic(motion.label) * smooth(0.6, 1, p),
186
+ side: motion.side,
187
+ pulse: pulseHz > 0 ? 0.5 + 0.5 * Math.sin(seconds * Math.PI * 2 * pulseHz) : 0
188
+ };
189
+ }
190
+ function labelWanted(show, state, hovered) {
191
+ if (state === "hidden" || show === "never") return false;
192
+ if (show === "always") return true;
193
+ if (show === "open") return state === "open";
194
+ return hovered || state === "open";
195
+ }
196
+ function approach(value, target, dt, seconds) {
197
+ if (seconds <= 0) return target;
198
+ const step2 = dt / seconds;
199
+ return value < target ? Math.min(target, value + step2) : Math.max(target, value - step2);
200
+ }
201
+ function step(motion, target, dt, timing) {
202
+ motion.presence = approach(
203
+ motion.presence,
204
+ target.presence,
205
+ dt,
206
+ timing.appear
207
+ );
208
+ motion.open = approach(motion.open, target.open, dt, timing.open);
209
+ motion.hover = approach(motion.hover, target.hover, dt, timing.hover);
210
+ motion.label = approach(motion.label, target.label, dt, timing.label);
211
+ motion.side = approach(motion.side, target.side, dt, timing.side);
212
+ }
213
+ function phaseOf(motion, state) {
214
+ if (state === "hidden") return motion.presence > 0 ? "leaving" : "hidden";
215
+ if (motion.presence < 1) return "entering";
216
+ if (state === "open") return motion.open < 1 ? "opening" : "open";
217
+ return motion.open > 0 ? "closing" : "closed";
218
+ }
219
+ function vector(value) {
24
220
  return value instanceof Vector3 ? value.clone() : new Vector3(value[0], value[1], value[2]);
25
221
  }
26
- function vec3Of(vector) {
27
- return [vector.x, vector.y, vector.z];
222
+ function vec3Of(v) {
223
+ return [v.x, v.y, v.z];
28
224
  }
29
225
  function createHotspot(options) {
30
226
  const {
31
227
  id,
32
- label,
33
228
  parent,
229
+ render = false,
230
+ style = "orb",
34
231
  color = HOTSPOT_COLOR,
232
+ openColor,
233
+ glow = 0,
35
234
  sizing = "screen",
36
- size = sizing === "screen" ? 0.012 : 0.03,
37
- pulse = true
235
+ size = sizing === "screen" ? 0.015 : 0.03,
236
+ showLabel = "never",
237
+ labelOpacity = 0.9,
238
+ labelSize = 1
38
239
  } = 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;
240
+ let label = options.label;
241
+ const intensity = options.intensity ?? (render ? INTENSITY[style] : 1);
242
+ const pulseHz = options.pulse === false ? 0 : typeof options.pulse === "number" ? Math.max(0, options.pulse) : PULSE_HZ;
243
+ const timing = {
244
+ appear: options.timing?.appear ?? 0.8,
245
+ open: options.timing?.open ?? 0.45,
246
+ label: options.timing?.label ?? 0.3,
247
+ hover: 0.15,
248
+ side: 0.35
249
+ };
250
+ const labelSide = options.labelSide ?? "auto";
251
+ const anchor = options.anchor ? vector(options.anchor) : void 0;
252
+ const anchored = anchor !== void 0;
253
+ const given = options.position ? vector(options.position) : void 0;
254
+ const direction = new Vector3();
255
+ if (options.direction) direction.copy(vector(options.direction));
256
+ else if (anchor && given) direction.copy(given).sub(anchor);
257
+ else direction.copy(anchored ? UP : FRONT);
258
+ if (direction.lengthSq() < 1e-12) direction.copy(anchored ? UP : FRONT);
259
+ direction.normalize();
260
+ let distance = options.distance ?? (anchor && given ? given.distanceTo(anchor) : anchored ? 0.1 : 0);
44
261
  const object = new Group();
45
262
  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 })
263
+ if (!render) object.userData.bakery3 = { exclude: true };
264
+ object.position.copy(anchor ?? given ?? new Vector3());
265
+ const base = new Color(color);
266
+ const opened = openColor ? new Color(openColor) : void 0;
267
+ const custom = options.material;
268
+ const materials = [];
269
+ const geometries = [];
270
+ const materialFor = (name, preset) => {
271
+ let material;
272
+ if (typeof custom === "function")
273
+ material = custom({ name, color: base.clone() });
274
+ else if (custom && name === "core") material = custom;
275
+ material ??= preset();
276
+ if (!render) material.toneMapped = false;
277
+ materials.push(material);
278
+ return material;
279
+ };
280
+ const part = (mesh, name) => {
281
+ mesh.name = name;
282
+ if (render) mesh.userData.bakery3 = { castsShadow: false };
283
+ return mesh;
284
+ };
285
+ const glowing = (strength, extra = {}) => new MeshStandardMaterial({
286
+ // A dark base under the emission: the part reads as light, not as a lit thing.
287
+ color: base.clone().multiplyScalar(0.12),
288
+ emissive: base.clone(),
289
+ emissiveIntensity: strength,
290
+ roughness: 0.45,
291
+ metalness: 0,
292
+ ...extra
293
+ });
294
+ const sphere = () => {
295
+ const geometry = new SphereGeometry(1, 32, 20);
296
+ geometries.push(geometry);
297
+ return geometry;
298
+ };
299
+ const rig = new Group();
300
+ rig.name = "rig";
301
+ const head = new Group();
302
+ head.name = "head";
303
+ head.add(rig);
304
+ object.add(head);
305
+ const coreRadius = style === "glass" ? GLASS_CORE : style === "ring" ? RING_CORE : 1;
306
+ const core = part(
307
+ new Mesh(
308
+ sphere(),
309
+ materialFor(
310
+ "core",
311
+ () => style === "solid" ? new MeshStandardMaterial({
312
+ color: base.clone(),
313
+ emissive: base.clone(),
314
+ emissiveIntensity: intensity,
315
+ roughness: 0.22,
316
+ metalness: 0
317
+ }) : glowing(intensity)
318
+ )
319
+ ),
320
+ "core"
51
321
  );
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,
322
+ rig.add(core);
323
+ let halo;
324
+ if (style === "orb") {
325
+ halo = part(
326
+ new Mesh(
327
+ sphere(),
328
+ materialFor(
329
+ "halo",
330
+ () => glowing(intensity * 0.5, {
331
+ color: new Color(0, 0, 0),
332
+ transparent: true,
333
+ opacity: HALO_OPACITY,
334
+ depthWrite: false,
335
+ roughness: 1
336
+ })
337
+ )
338
+ ),
339
+ "halo"
340
+ );
341
+ halo.renderOrder = 1;
342
+ rig.add(halo);
343
+ }
344
+ let ring;
345
+ if (style === "ring") {
346
+ const geometry = new TorusGeometry(1, RING_TUBE / RING_RADIUS, 12, 64);
347
+ geometries.push(geometry);
348
+ ring = part(
349
+ new Mesh(
350
+ geometry,
351
+ materialFor(
352
+ "ring",
353
+ () => glowing(intensity, { transparent: true, opacity: 0.9 })
354
+ )
355
+ ),
356
+ "ring"
357
+ );
358
+ rig.add(ring);
359
+ }
360
+ let shell;
361
+ if (style === "glass") {
362
+ shell = part(
363
+ new Mesh(
364
+ sphere(),
365
+ materialFor(
366
+ "shell",
367
+ () => new MeshPhysicalMaterial({
368
+ color: "#ffffff",
369
+ transmission: 1,
370
+ roughness: 0.04,
371
+ ior: 1.5,
372
+ thickness: 0.02,
373
+ metalness: 0,
374
+ specularIntensity: 1
375
+ })
376
+ )
377
+ ),
378
+ "shell"
379
+ );
380
+ rig.add(shell);
381
+ }
382
+ let tether;
383
+ if (anchored && options.tether !== false) {
384
+ const geometry = new CylinderGeometry(1, 1, 1, 10, 1, true);
385
+ geometry.translate(0, 0.5, 0);
386
+ geometries.push(geometry);
387
+ tether = part(
388
+ new Mesh(
389
+ geometry,
390
+ materialFor("tether", () => glowing(intensity * 0.6))
391
+ ),
392
+ "tether"
393
+ );
394
+ tether.quaternion.setFromUnitVectors(UP, direction);
395
+ object.add(tether);
396
+ }
397
+ let dot;
398
+ if (anchored && options.dot !== false) {
399
+ dot = part(
400
+ new Mesh(
401
+ sphere(),
402
+ materialFor("dot", () => glowing(intensity))
403
+ ),
404
+ "dot"
405
+ );
406
+ object.add(dot);
407
+ }
408
+ let labelMesh;
409
+ let labelMaterial;
410
+ let labelMap;
411
+ let labelAspect = 3;
412
+ const labelLook = () => ({
413
+ text: label ?? id,
414
+ font: options.labelFont ?? LABEL_FONT,
415
+ color: options.labelColor ?? "#ffffff",
416
+ background: options.labelBackground ?? "#0b0c12"
417
+ });
418
+ const drawLabel = () => {
419
+ if (!labelMesh || !labelMaterial) return;
420
+ const raster = rasterizeLabel(labelLook());
421
+ labelMap?.dispose();
422
+ labelMap = raster.texture;
423
+ labelAspect = raster.aspect;
424
+ labelMaterial.emissiveMap = labelMap;
425
+ labelMaterial.needsUpdate = true;
426
+ const old = labelMesh.geometry;
427
+ labelMesh.geometry = pillGeometry(labelAspect);
428
+ old.dispose();
429
+ };
430
+ if (showLabel !== "never") {
431
+ labelMaterial = new MeshPhysicalMaterial({
432
+ color: 0,
433
+ emissive: 16777215,
434
+ emissiveIntensity: render ? LABEL_GLOW : 1,
435
+ roughness: 1,
436
+ metalness: 0,
437
+ // No sheen of the studio across the text.
438
+ specularIntensity: 0,
59
439
  transparent: true,
60
- opacity: HALO_OPACITY,
440
+ opacity: 0,
61
441
  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
- })
442
+ });
443
+ if (!render) labelMaterial.toneMapped = false;
444
+ materials.push(labelMaterial);
445
+ labelMesh = part(
446
+ new Mesh(pillGeometry(labelAspect), labelMaterial),
447
+ "label"
85
448
  );
86
- line.name = "tether";
87
- object.add(line);
449
+ labelMesh.renderOrder = 2;
450
+ const height = LABEL_HEIGHT * labelSize;
451
+ labelMesh.scale.set(height, height, 1);
452
+ rig.add(labelMesh);
453
+ drawLabel();
454
+ const fonts = typeof document !== "undefined" ? document.fonts : void 0;
455
+ const font = labelLook().font;
456
+ if (fonts && !fonts.check(font))
457
+ void fonts.load(font).then(
458
+ () => !disposed && drawLabel(),
459
+ () => void 0
460
+ );
461
+ }
462
+ let light;
463
+ if (glow > 0) {
464
+ light = new PointLight((opened ?? base).clone(), glow * CLOSED_GLOW, 0, 2);
465
+ light.name = "glow";
466
+ light.castShadow = false;
467
+ object.add(light);
88
468
  }
89
469
  parent?.add(object);
90
- let selected = false;
470
+ let state = options.state ?? "closed";
91
471
  let hovered = false;
472
+ let side = labelSide === "left" ? "left" : "right";
473
+ let sided = labelSide !== "auto";
474
+ let fitted = size;
475
+ let facing = [0, 0, 0];
476
+ let eye = null;
477
+ let clock;
92
478
  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;
479
+ const targets = (next, hover, labelOn) => ({
480
+ presence: next === "hidden" ? 0 : 1,
481
+ open: next === "open" ? 1 : 0,
482
+ hover: hover ? 1 : 0,
483
+ label: labelWanted(showLabel, next, hover) ? 1 : 0,
484
+ side: labelOn === "left" ? 1 : 0
485
+ });
486
+ const motion = targets(state, false, side);
487
+ const worldScale = () => {
488
+ object.updateWorldMatrix(true, false);
489
+ object.getWorldScale(SCALE);
490
+ return Math.max(1e-9, (SCALE.x + SCALE.y + SCALE.z) / 3);
491
+ };
492
+ const headLocal = (extend, ws) => direction.clone().multiplyScalar(distance * extend / ws);
493
+ const orbRadius = (camera, headWorld) => {
494
+ if (sizing === "world" || !camera)
495
+ return sizing === "world" ? size : fitted;
496
+ const perspective = camera.isPerspectiveCamera === true;
497
+ const ortho = camera.isOrthographicCamera === true;
498
+ if (!perspective && !ortho) return fitted;
499
+ let visibleHeight;
500
+ if (perspective) {
501
+ const cam = camera;
502
+ camera.updateMatrixWorld();
503
+ camera.getWorldPosition(CAMERA);
504
+ camera.getWorldDirection(FORWARD);
505
+ const depth = Math.max(
506
+ 1e-3,
507
+ WORLD.copy(headWorld).sub(CAMERA).dot(FORWARD)
508
+ );
509
+ visibleHeight = 2 * depth * Math.tan(cam.fov * Math.PI / 360);
510
+ } else {
511
+ const cam = camera;
512
+ visibleHeight = (cam.top - cam.bottom) / cam.zoom;
513
+ }
514
+ return Math.max(1e-6, size * visibleHeight);
515
+ };
516
+ const facingFor = (cameraQuaternion) => {
517
+ head.updateWorldMatrix(true, false);
518
+ head.getWorldQuaternion(ORIENT);
519
+ VIEW.copy(ORIENT).invert().multiply(cameraQuaternion);
520
+ EULER.setFromQuaternion(VIEW, rig.rotation.order);
521
+ return [EULER.x, EULER.y, EULER.z];
522
+ };
523
+ const eyeFor = (camera, headWorld, radius) => {
524
+ if (camera.isPerspectiveCamera !== true) return null;
525
+ camera.updateMatrixWorld();
526
+ camera.getWorldPosition(CAMERA);
527
+ camera.getWorldQuaternion(VIEW);
528
+ const local = CAMERA.sub(headWorld).applyQuaternion(VIEW.invert()).divideScalar(Math.max(radius, 1e-9));
529
+ return [local.x, local.y, local.z];
530
+ };
531
+ const labelGap = REACH[style] + 0.35;
532
+ const sideFor = (current, camera, headWorld, radius) => {
533
+ if (labelSide !== "auto" || !labelMesh) return current;
534
+ camera.updateMatrixWorld();
535
+ camera.getWorldQuaternion(VIEW);
536
+ RIGHT.set(1, 0, 0).applyQuaternion(VIEW);
537
+ const reach = (labelGap + LABEL_HEIGHT * labelSize * labelAspect) * radius;
538
+ const x = WORLD.copy(headWorld).project(camera).x;
539
+ const edge = EDGE.copy(headWorld).addScaledVector(RIGHT, reach).project(camera).x;
540
+ if (!Number.isFinite(x) || !Number.isFinite(edge)) return current;
541
+ return labelSideFor(current, x, Math.abs(edge - x));
542
+ };
543
+ const mixColor = (open) => opened ? base.clone().lerp(opened, open) : base;
544
+ const writes = (pose, frame) => {
545
+ const { radius, ws } = frame;
546
+ const out = [];
547
+ const emissive = (mesh, strength2, darken = 0.12) => {
548
+ if (!mesh) return;
549
+ const material = mesh.material;
550
+ if (typeof material.emissiveIntensity === "number")
551
+ out.push({
552
+ object: mesh,
553
+ property: "emissiveIntensity",
554
+ value: [strength2]
555
+ });
556
+ const nodes = material;
557
+ if (opened && material.emissive?.isColor && !nodes.emissiveNode) {
558
+ const c = mixColor(pose.open);
559
+ out.push({
560
+ object: mesh,
561
+ property: "emissive",
562
+ value: [c.r, c.g, c.b]
563
+ });
564
+ if (material.color?.isColor && darken > 0 && !nodes.colorNode)
565
+ out.push({
566
+ object: mesh,
567
+ property: "color",
568
+ value: [c.r * darken, c.g * darken, c.b * darken]
569
+ });
570
+ }
571
+ };
572
+ const uniform = (mesh, s) => {
573
+ if (!mesh) return;
574
+ const k = Math.max(s, FLOOR);
575
+ out.push({ object: mesh, property: "scale", value: [k, k, k] });
576
+ };
577
+ const rigScale = radius / ws;
578
+ out.push({
579
+ object: rig,
580
+ property: "scale",
581
+ value: [rigScale, rigScale, rigScale]
582
+ });
583
+ out.push({ object: rig, property: "rotation", value: [...frame.facing] });
584
+ out.push({
585
+ object: head,
586
+ property: "position",
587
+ value: vec3Of(headLocal(pose.extend, ws))
588
+ });
589
+ const h = pose.head;
590
+ const open = pose.open;
591
+ const hover = pose.hover;
592
+ const breath = pose.pulse * (1 - open);
593
+ const emphasis = 1 + 0.35 * open + 0.1 * hover;
594
+ const strength = intensity * (1 + 1.2 * open + 0.3 * hover);
595
+ uniform(
596
+ core,
597
+ h * coreRadius * (style === "glass" ? 1 + 0.35 * open : emphasis)
598
+ );
599
+ emissive(core, strength, style === "solid" ? 1 : 0.12);
600
+ if (halo) {
601
+ uniform(halo, h * (HALO_SCALE + HALO_PULSE * breath + 0.5 * open));
602
+ out.push({
603
+ object: halo,
604
+ property: "opacity",
605
+ value: [HALO_OPACITY * (1 + 0.6 * hover) * (1 - 0.25 * open)]
606
+ });
607
+ emissive(halo, strength * 0.5, 0);
608
+ }
609
+ if (ring) {
610
+ uniform(
611
+ ring,
612
+ h * RING_RADIUS * (1 + 0.45 * open + 0.12 * breath + 0.08 * hover)
613
+ );
614
+ out.push({
615
+ object: ring,
616
+ property: "opacity",
617
+ value: [0.9 - 0.35 * breath]
618
+ });
619
+ emissive(ring, strength);
620
+ }
621
+ if (shell) uniform(shell, h * (1 + 0.05 * hover));
622
+ if (labelMesh) {
623
+ const width = LABEL_HEIGHT * labelSize * labelAspect;
624
+ const slide = (1 - pose.label) * LABEL_SLIDE;
625
+ const left = pose.side >= 0.5;
626
+ const swap = Math.abs(2 * pose.side - 1);
627
+ const x = left ? -(labelGap + slide) - width : labelGap + slide;
628
+ const height = LABEL_HEIGHT * labelSize;
629
+ const k = frame.eye ? LABEL_LIFT : 0;
630
+ const [ex, ey, ez] = frame.eye ?? [0, 0, 0];
631
+ out.push({
632
+ object: labelMesh,
633
+ property: "position",
634
+ value: [x * (1 - k) + k * ex, k * ey, k * ez]
635
+ });
636
+ out.push({
637
+ object: labelMesh,
638
+ property: "scale",
639
+ value: [height * (1 - k), height * (1 - k), 1]
640
+ });
641
+ out.push({
642
+ object: labelMesh,
643
+ property: "opacity",
644
+ value: [labelOpacity * pose.label * swap]
645
+ });
646
+ }
647
+ if (tether) {
648
+ const thickness = Math.max(radius * TETHER_RADIUS / ws, 1e-9);
649
+ const length = Math.max(
650
+ distance * pose.extend / ws,
651
+ FLOOR * radius / ws
652
+ );
653
+ const visible = pose.extend > 0 ? 1 : FLOOR;
654
+ out.push({
655
+ object: tether,
656
+ property: "scale",
657
+ value: [thickness * visible, length, thickness * visible]
658
+ });
659
+ emissive(tether, intensity * 0.6 * (1 + 0.6 * open));
660
+ }
661
+ if (dot) {
662
+ uniform(dot, pose.dot * radius * DOT_RADIUS / ws);
663
+ emissive(dot, intensity * (1 + 0.6 * open));
664
+ }
665
+ if (light) {
666
+ const presence = Math.min(1, h);
667
+ out.push({
668
+ object: light,
669
+ property: "intensity",
670
+ value: [
671
+ Math.max(
672
+ LIGHT_FLOOR,
673
+ glow * presence * (CLOSED_GLOW + (1 - CLOSED_GLOW) * open)
674
+ )
675
+ ]
676
+ });
677
+ }
678
+ return out;
679
+ };
680
+ const apply = (seconds) => {
681
+ const ws = worldScale();
682
+ const pose = poseOf(motion, seconds, anchored, pulseHz);
683
+ const frame = {
684
+ radius: sizing === "world" ? size : fitted,
685
+ ws,
686
+ facing,
687
+ eye
688
+ };
689
+ for (const write of writes(pose, frame))
690
+ OBJECT_PROPERTIES[write.property].write(write.object, write.value);
691
+ };
692
+ const placeLight = () => {
693
+ if (light) light.position.copy(headLocal(1, worldScale()));
694
+ };
695
+ placeLight();
696
+ const internals = {
697
+ writes,
698
+ orbRadius,
699
+ facing: facingFor,
700
+ eye: eyeFor,
701
+ sideFor,
702
+ targets,
703
+ headLocal,
704
+ worldScale,
705
+ anchored,
706
+ pulseHz,
707
+ timing,
708
+ labelSide,
709
+ driven: null,
710
+ render
100
711
  };
101
712
  const hotspot = {
102
713
  id,
103
- label,
714
+ get label() {
715
+ return label;
716
+ },
104
717
  object,
718
+ get state() {
719
+ return state;
720
+ },
721
+ get phase() {
722
+ return phaseOf(motion, state);
723
+ },
105
724
  get selected() {
106
- return selected;
725
+ return state === "open";
107
726
  },
108
727
  get hovered() {
109
728
  return hovered;
110
729
  },
730
+ get distance() {
731
+ return distance;
732
+ },
733
+ get labelSide() {
734
+ return side;
735
+ },
736
+ get driven() {
737
+ return internals.driven !== null;
738
+ },
739
+ setState(next, opts = {}) {
740
+ state = next;
741
+ if (opts.instant) {
742
+ Object.assign(motion, targets(next, hovered, side));
743
+ if (!internals.driven) apply(clock ?? 0);
744
+ }
745
+ },
111
746
  setSelected(on) {
112
- if (selected === on) return;
113
- selected = on;
114
- applyLook();
747
+ hotspot.setState(on ? "open" : "closed");
748
+ },
749
+ setHovered(on) {
750
+ hovered = on;
751
+ },
752
+ setLabel(text) {
753
+ label = text;
754
+ drawLabel();
755
+ },
756
+ setDistance(meters) {
757
+ distance = Math.max(0, meters);
758
+ placeLight();
759
+ if (!internals.driven) apply(clock ?? 0);
760
+ },
761
+ setDirection(next) {
762
+ const v = vector(next);
763
+ if (v.lengthSq() < 1e-12) return;
764
+ direction.copy(v.normalize());
765
+ tether?.quaternion.setFromUnitVectors(UP, direction);
766
+ placeLight();
767
+ if (!internals.driven) apply(clock ?? 0);
115
768
  },
116
769
  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;
770
+ if (disposed || internals.driven) return;
771
+ const ws = worldScale();
772
+ object.updateWorldMatrix(true, false);
773
+ const headWorld = headLocal(1, ws).applyMatrix4(object.matrixWorld);
774
+ if (sizing === "screen") fitted = orbRadius(camera, headWorld);
775
+ if (typeof camera.updateMatrixWorld !== "function") return;
776
+ camera.updateMatrixWorld();
777
+ facing = facingFor(camera.getWorldQuaternion(new Quaternion()));
778
+ const radius = sizing === "world" ? size : fitted;
779
+ eye = eyeFor(camera, headWorld, radius);
780
+ side = sideFor(side, camera, headWorld, radius);
781
+ if (!sided) {
782
+ sided = true;
783
+ motion.side = side === "left" ? 1 : 0;
132
784
  }
133
- rig.scale.setScalar(Math.max(1e-6, size * visibleHeight));
134
785
  },
135
786
  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));
787
+ if (disposed || internals.driven) return;
788
+ const dt = clock === void 0 ? 0 : Math.max(0, seconds - clock);
789
+ clock = seconds;
790
+ step(motion, targets(state, hovered, side), dt, timing);
791
+ apply(seconds);
140
792
  },
141
793
  dispose() {
142
794
  if (disposed) return;
143
795
  disposed = true;
144
796
  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();
797
+ for (const geometry of geometries) geometry.dispose();
798
+ labelMesh?.geometry.dispose();
799
+ for (const material of materials) material.dispose();
800
+ labelMap?.dispose();
801
+ light?.dispose();
151
802
  }
152
803
  };
153
804
  PARTS.set(hotspot, {
154
- orb,
805
+ core,
806
+ orb: core,
155
807
  halo,
808
+ ring,
809
+ shell,
810
+ label: labelMesh,
156
811
  rig,
157
- line,
158
- setHovered(on) {
159
- if (hovered === on) return;
160
- hovered = on;
161
- applyLook();
162
- }
812
+ head,
813
+ tether,
814
+ line: tether,
815
+ dot,
816
+ light,
817
+ setHovered: (on) => hotspot.setHovered(on)
163
818
  });
819
+ INTERNALS.set(hotspot, internals);
820
+ apply(0);
164
821
  return hotspot;
165
822
  }
823
+ function hotspotFromHit(hit, options = {}) {
824
+ const { along = "normal", center, space } = options;
825
+ const point = hit.point.clone();
826
+ const out = new Vector3();
827
+ if (along === "center" && center) {
828
+ out.copy(point).sub(center);
829
+ } else if (hit.face) {
830
+ out.copy(hit.face.normal).applyMatrix3(new Matrix3().getNormalMatrix(hit.object.matrixWorld));
831
+ }
832
+ if (along === "normal" && !hit.face && center) out.copy(point).sub(center);
833
+ if (out.lengthSq() < 1e-12) out.copy(UP);
834
+ out.normalize();
835
+ if (space) {
836
+ space.updateWorldMatrix(true, false);
837
+ const inverse = new Matrix4().copy(space.matrixWorld).invert();
838
+ const anchor = point.clone().applyMatrix4(inverse);
839
+ const tip = hit.point.clone().add(out).applyMatrix4(inverse);
840
+ return { anchor, direction: tip.sub(anchor).normalize() };
841
+ }
842
+ return { anchor: point, direction: out };
843
+ }
166
844
  function hotspotSpecOf(hotspot) {
845
+ const internals = INTERNALS.get(hotspot);
846
+ if (!internals)
847
+ throw new Error(
848
+ "@oeave/bakery3/hotspots: that hotspot was not made by createHotspot()."
849
+ );
167
850
  const { object } = hotspot;
168
851
  object.updateWorldMatrix(true, false);
169
- const spec = {
170
- id: hotspot.id,
171
- position: vec3Of(object.getWorldPosition(new Vector3()))
172
- };
852
+ const tip = internals.headLocal(1, internals.worldScale()).applyMatrix4(object.matrixWorld);
853
+ const spec = { id: hotspot.id, position: vec3Of(tip) };
173
854
  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
855
+ if (internals.anchored)
856
+ spec.anchor = vec3Of(object.getWorldPosition(new Vector3()));
857
+ return spec;
858
+ }
859
+ var OWNED = /* @__PURE__ */ new WeakMap();
860
+ var TRANSFORMS = ["position", "rotation", "scale"];
861
+ var idOfEvent = (event) => typeof event.hotspot === "string" ? event.hotspot : event.hotspot.id;
862
+ function animateHotspots(timeline, hotspots, options = {}) {
863
+ releaseHotspots(timeline);
864
+ const { events = [], initial = "hidden", loop = true } = options;
865
+ const byId = new Map(hotspots.map((hotspot) => [hotspot.id, hotspot]));
866
+ for (const event of events) {
867
+ const id = idOfEvent(event);
868
+ if (!byId.has(id))
869
+ throw new Error(
870
+ `@oeave/bakery3/hotspots: an event names "${id}", which is not one of the hotspots passed to animateHotspots().`
871
+ );
872
+ if (event.state === void 0 && event.hover === void 0)
873
+ throw new Error(
874
+ `@oeave/bakery3/hotspots: the event for "${id}" at ${event.at} s changes nothing. Give it a state, a hover, or both.`
875
+ );
876
+ }
877
+ const fps = timeline.fps;
878
+ const duration = timeline.duration;
879
+ const frames = Math.max(1, Math.round(duration * fps));
880
+ const camera = timeline.camera;
881
+ const fovTrack = timeline.tracks.find(
882
+ (track) => track.target === "camera" && track.property === "fov"
883
+ );
884
+ const owned = [];
885
+ const views = [];
886
+ for (let frame = 0; frame < frames; frame++) {
887
+ const time = frame / fps;
888
+ if (!camera) {
889
+ views.push(void 0);
890
+ continue;
891
+ }
892
+ const cameraPose = timeline.cameraPoseAt(time);
893
+ if (!cameraPose && !fovTrack) {
894
+ views.push(camera);
895
+ continue;
896
+ }
897
+ const view = camera.clone();
898
+ if (cameraPose) {
899
+ view.position.set(...cameraPose.position);
900
+ view.quaternion.set(...cameraPose.quaternion);
901
+ }
902
+ const fov = fovTrack ? timeline.valueAt(fovTrack, time) : void 0;
903
+ if (fov && view.isPerspectiveCamera) {
904
+ view.fov = fov[0];
905
+ view.updateProjectionMatrix();
906
+ }
907
+ view.updateMatrixWorld(true);
908
+ views.push(view);
909
+ }
910
+ for (const hotspot of hotspots) {
911
+ const internals = INTERNALS.get(hotspot);
912
+ if (!internals)
913
+ throw new Error(
914
+ "@oeave/bakery3/hotspots: that hotspot was not made by createHotspot()."
915
+ );
916
+ if (!internals.render)
917
+ throw new Error(
918
+ `@oeave/bakery3/hotspots: hotspot "${hotspot.id}" does not render. Create it with { render: true } to play it in a video.`
919
+ );
920
+ const mine = events.filter((event) => idOfEvent(event) === hotspot.id).sort((a, b) => a.at - b.at);
921
+ let state = typeof initial === "string" ? initial : initial[hotspot.id] ?? "hidden";
922
+ let hovered = false;
923
+ let side = internals.labelSide === "left" ? "left" : "right";
924
+ let motion;
925
+ let now = 0;
926
+ let next = 0;
927
+ const pulseHz = loop && internals.pulseHz > 0 && duration > 0 ? Math.max(1, Math.round(internals.pulseHz * duration)) / duration : internals.pulseHz;
928
+ const ws = internals.worldScale();
929
+ hotspot.object.updateWorldMatrix(true, false);
930
+ const matrix = hotspot.object.matrixWorld.clone();
931
+ const ownTracks = [];
932
+ const samples = /* @__PURE__ */ new Map();
933
+ for (let frame = 0; frame < frames; frame++) {
934
+ const time = frame / fps;
935
+ const view = views[frame];
936
+ const restWorld = internals.headLocal(1, ws).applyMatrix4(matrix);
937
+ const restRadius = internals.orbRadius(view, restWorld);
938
+ if (view) side = internals.sideFor(side, view, restWorld, restRadius);
939
+ if (!motion) motion = internals.targets(state, hovered, side);
940
+ const advance = (to) => {
941
+ const dt = to - now;
942
+ if (dt <= 0) return;
943
+ step(
944
+ motion,
945
+ internals.targets(state, hovered, side),
946
+ dt,
947
+ internals.timing
948
+ );
949
+ now = to;
950
+ };
951
+ while (next < mine.length && mine[next].at <= time) {
952
+ const event = mine[next];
953
+ advance(Math.max(0, event.at));
954
+ if (event.state !== void 0) state = event.state;
955
+ if (event.hover !== void 0) hovered = event.hover;
956
+ next++;
957
+ }
958
+ advance(time);
959
+ const pose = poseOf(motion, time, internals.anchored, pulseHz);
960
+ const headWorld = internals.headLocal(pose.extend, ws).applyMatrix4(matrix);
961
+ const radius = internals.orbRadius(view, headWorld);
962
+ const facing = view ? internals.facing(view.getWorldQuaternion(new Quaternion())) : [0, 0, 0];
963
+ const eye = view ? internals.eye(view, headWorld, radius) : null;
964
+ for (const write of internals.writes(pose, { radius, ws, facing, eye })) {
965
+ const key = `${write.object.uuid}:${write.property}`;
966
+ let entry = samples.get(key);
967
+ if (!entry) {
968
+ entry = {
969
+ object: write.object,
970
+ property: write.property,
971
+ values: []
972
+ };
973
+ samples.set(key, entry);
974
+ }
975
+ entry.values.push(write.value);
976
+ }
977
+ }
978
+ for (const { object, property, values } of samples.values()) {
979
+ const first = values[0];
980
+ const constant = values.every(
981
+ (value) => value.every((n, i) => Math.abs(n - first[i]) < 1e-7)
982
+ );
983
+ if (constant) {
984
+ OBJECT_PROPERTIES[property].write(object, first);
985
+ continue;
986
+ }
987
+ if (property === "rotation") unwrapEulers(values);
988
+ const track = timeline.track(object, property);
989
+ track.keyframes.length = 0;
990
+ values.forEach(
991
+ (value, frame) => track.keyframes.push({
992
+ time: frame / fps,
993
+ value: value.map(short),
994
+ easing: "linear"
995
+ })
996
+ );
997
+ track.label = `${hotspot.id} \xB7 ${object.name} ${property}`;
998
+ owned.push(track);
999
+ ownTracks.push(track);
1000
+ }
1001
+ const moving = new Set(
1002
+ ownTracks.filter((track) => TRANSFORMS.includes(track.property)).map((track) => track.target)
179
1003
  );
180
- spec.anchor = vec3Of(object.localToWorld(local));
1004
+ const underMoving = (node) => {
1005
+ for (let up = node.parent; up && up !== hotspot.object; up = up.parent)
1006
+ if (moving.has(up)) return true;
1007
+ return false;
1008
+ };
1009
+ const parts = hotspotParts(hotspot);
1010
+ const nodes = [
1011
+ parts.head,
1012
+ parts.rig,
1013
+ parts.core,
1014
+ parts.halo,
1015
+ parts.ring,
1016
+ parts.shell,
1017
+ parts.label,
1018
+ parts.tether,
1019
+ parts.dot
1020
+ ].filter(
1021
+ (node) => node !== void 0 && underMoving(node)
1022
+ );
1023
+ for (const node of nodes)
1024
+ for (const property of TRANSFORMS) {
1025
+ if (ownTracks.some((t) => t.target === node && t.property === property))
1026
+ continue;
1027
+ const value = OBJECT_PROPERTIES[property].read(node).map(short);
1028
+ const track = timeline.track(node, property);
1029
+ track.keyframes.length = 0;
1030
+ track.keyframes.push(
1031
+ { time: 0, value: [...value], easing: "linear" },
1032
+ { time: duration, value: [...value], easing: "linear" }
1033
+ );
1034
+ track.label = `${hotspot.id} \xB7 ${node.name} ${property}`;
1035
+ owned.push(track);
1036
+ ownTracks.push(track);
1037
+ }
1038
+ internals.driven = timeline;
1039
+ }
1040
+ OWNED.set(timeline, { tracks: owned, hotspots });
1041
+ timeline.duration = timeline.duration;
1042
+ timeline.seek(timeline.time);
1043
+ return {
1044
+ tracks: owned.length,
1045
+ release: () => releaseHotspots(timeline)
1046
+ };
1047
+ }
1048
+ var short = (n) => Number(n.toPrecision(6));
1049
+ function unwrapEulers(values) {
1050
+ for (let i = 1; i < values.length; i++) {
1051
+ const previous = values[i - 1];
1052
+ const current = values[i];
1053
+ for (let axis = 0; axis < 3; axis++) {
1054
+ let angle = current[axis];
1055
+ while (angle - previous[axis] > Math.PI) angle -= Math.PI * 2;
1056
+ while (angle - previous[axis] < -Math.PI) angle += Math.PI * 2;
1057
+ current[axis] = angle;
1058
+ }
1059
+ }
1060
+ }
1061
+ function releaseHotspots(timeline) {
1062
+ const owned = OWNED.get(timeline);
1063
+ if (!owned) return;
1064
+ OWNED.delete(timeline);
1065
+ for (const track of owned.tracks) {
1066
+ if (timeline.tracks.includes(track)) timeline.removeTrack(track.id);
1067
+ }
1068
+ for (const hotspot of owned.hotspots) {
1069
+ const internals = INTERNALS.get(hotspot);
1070
+ if (internals?.driven === timeline) internals.driven = null;
181
1071
  }
182
- return spec;
183
1072
  }
184
1073
  var CLICK_SLOP_PX = 6;
185
1074
  function attachHotspots(options) {
@@ -188,11 +1077,11 @@ function attachHotspots(options) {
188
1077
  const pointer = new Vector2();
189
1078
  const targets = /* @__PURE__ */ new Map();
190
1079
  for (const hotspot of hotspots) {
191
- const { orb, halo } = hotspotParts(hotspot);
192
- targets.set(orb, hotspot);
193
- targets.set(halo, hotspot);
1080
+ const { core, halo, ring, shell, label } = hotspotParts(hotspot);
1081
+ for (const mesh of [core, halo, ring, shell, label])
1082
+ if (mesh) targets.set(mesh, hotspot);
194
1083
  }
195
- let selected = null;
1084
+ let selected = hotspots.find((hotspot) => hotspot.state === "open") ?? null;
196
1085
  let hovered = null;
197
1086
  let downAt = null;
198
1087
  let previousCursor = null;
@@ -207,15 +1096,18 @@ function attachHotspots(options) {
207
1096
  const hits = raycaster.intersectObjects([...targets.keys()], false);
208
1097
  for (const hit of hits) {
209
1098
  const hotspot = targets.get(hit.object);
210
- if (hotspot && hotspot.object.visible) return hotspot;
1099
+ if (hit.object === hotspotParts(hotspot).label && hit.object.material.opacity < 0.05)
1100
+ continue;
1101
+ if (hotspot && hotspot.object.visible && hotspot.state !== "hidden")
1102
+ return hotspot;
211
1103
  }
212
1104
  return null;
213
1105
  };
214
1106
  const setHovered = (next) => {
215
1107
  if (next === hovered) return;
216
- if (hovered) hotspotParts(hovered).setHovered(false);
1108
+ hovered?.setHovered(false);
217
1109
  hovered = next;
218
- if (hovered) hotspotParts(hovered).setHovered(true);
1110
+ hovered?.setHovered(true);
219
1111
  if (hovered) {
220
1112
  if (previousCursor === null) previousCursor = domElement.style.cursor;
221
1113
  domElement.style.cursor = "pointer";
@@ -232,7 +1124,10 @@ function attachHotspots(options) {
232
1124
  selected?.setSelected(true);
233
1125
  onSelect?.(selected);
234
1126
  };
235
- const onPointerMove = (event) => setHovered(pick(event));
1127
+ const onPointerMove = (event) => {
1128
+ if (event.pointerType === "touch") return;
1129
+ setHovered(pick(event));
1130
+ };
236
1131
  const onPointerLeave = () => setHovered(null);
237
1132
  const onPointerDown = (event) => {
238
1133
  if (event.button !== 0) return;
@@ -280,6 +1175,6 @@ function attachHotspots(options) {
280
1175
  };
281
1176
  }
282
1177
 
283
- export { attachHotspots, createHotspot, hotspotSpecOf };
1178
+ export { animateHotspots, attachHotspots, createHotspot, hotspotFromHit, hotspotSpecOf, releaseHotspots };
284
1179
  //# sourceMappingURL=index.js.map
285
1180
  //# sourceMappingURL=index.js.map