@oeave/bakery3 0.1.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.
- package/README.md +10 -10
- package/dist/bake/index.d.ts +3 -3
- package/dist/bake/index.js +4 -4
- package/dist/{bake-DaOlenyx.d.ts → bake-DZ-CJR6f.d.ts} +8 -1
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +3 -3
- package/dist/{chunk-TGFNQBKC.js → chunk-3T3FJOSD.js} +5 -5
- package/dist/chunk-3T3FJOSD.js.map +1 -0
- package/dist/{chunk-V3QINEEG.js → chunk-62M6XXNX.js} +58 -3
- package/dist/chunk-62M6XXNX.js.map +1 -0
- package/dist/{chunk-ARMTTZXZ.js → chunk-6FAMQZXB.js} +8 -8
- package/dist/chunk-6FAMQZXB.js.map +1 -0
- package/dist/{chunk-54WJDN5R.js → chunk-CSSJRP5W.js} +9 -6
- package/dist/chunk-CSSJRP5W.js.map +1 -0
- package/dist/{chunk-W6WQ5UP7.js → chunk-EN4ROBYQ.js} +32 -5
- package/dist/chunk-EN4ROBYQ.js.map +1 -0
- package/dist/chunk-KZLVTSBI.js +191 -0
- package/dist/chunk-KZLVTSBI.js.map +1 -0
- package/dist/{chunk-MD2U6S4N.js → chunk-LAKXC4WR.js} +218 -142
- package/dist/chunk-LAKXC4WR.js.map +1 -0
- package/dist/{chunk-HQTUFVMM.js → chunk-NEK5TBEH.js} +3 -3
- package/dist/chunk-NEK5TBEH.js.map +1 -0
- package/dist/chunk-OPM2L63B.js +253 -0
- package/dist/chunk-OPM2L63B.js.map +1 -0
- package/dist/{chunk-APBOWWSV.js → chunk-R5IUPOWI.js} +3 -3
- package/dist/chunk-R5IUPOWI.js.map +1 -0
- package/dist/{chunk-6YVQANMW.js → chunk-STRK4SZN.js} +262 -16
- package/dist/chunk-STRK4SZN.js.map +1 -0
- package/dist/{chunk-CX4WTVGH.js → chunk-WLEFNUOH.js} +411 -371
- package/dist/chunk-WLEFNUOH.js.map +1 -0
- package/dist/{chunk-25ZXSVQ3.js → chunk-X6GW43EX.js} +3 -3
- package/dist/{chunk-25ZXSVQ3.js.map → chunk-X6GW43EX.js.map} +1 -1
- package/dist/{chunk-WBIAFIHV.js → chunk-XK35ANPJ.js} +3 -3
- package/dist/{chunk-WBIAFIHV.js.map → chunk-XK35ANPJ.js.map} +1 -1
- package/dist/devtools/index.d.ts +3 -3
- package/dist/devtools/index.js +10 -9
- package/dist/hotspots/index.d.ts +237 -31
- package/dist/hotspots/index.js +1013 -118
- package/dist/hotspots/index.js.map +1 -1
- package/dist/index.d.ts +39 -8
- package/dist/index.js +11 -10
- package/dist/node/index.cjs +266 -141
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +152 -87
- package/dist/node/index.d.ts +27 -6
- package/dist/node/index.js +52 -3
- package/dist/node/index.js.map +1 -1
- package/dist/{prepare-C5qVvaw4.d.ts → prepare-BtjY4G3q.d.ts} +1 -1
- package/dist/presets/index.d.ts +37 -1
- package/dist/presets/index.js +4 -4
- package/dist/presets/index.js.map +1 -1
- package/dist/r3f/index.d.ts +4 -4
- package/dist/r3f/index.js +13 -12
- package/dist/r3f/index.js.map +1 -1
- package/dist/{room-S5TLCK4F.js → room-BM7Z7NKG.js} +6 -6
- package/dist/{room-S5TLCK4F.js.map → room-BM7Z7NKG.js.map} +1 -1
- package/dist/{rooms.gen-vhj6sFW5.d.ts → rooms.gen-DItzBR9k.d.ts} +112 -84
- package/dist/session-NPVBESWA.js +9 -0
- package/dist/{session-YCQS4MRH.js.map → session-NPVBESWA.js.map} +1 -1
- package/dist/{testRun-BIPXaVPb.d.ts → testRun-20OARnQr.d.ts} +12 -3
- package/dist/tsl/index.d.ts +14 -1
- package/dist/tsl/index.js +8 -2
- package/dist/tsl/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-54WJDN5R.js.map +0 -1
- package/dist/chunk-6YVQANMW.js.map +0 -1
- package/dist/chunk-AF5BIELI.js +0 -70
- package/dist/chunk-AF5BIELI.js.map +0 -1
- package/dist/chunk-APBOWWSV.js.map +0 -1
- package/dist/chunk-ARMTTZXZ.js.map +0 -1
- package/dist/chunk-CX4WTVGH.js.map +0 -1
- package/dist/chunk-HQTUFVMM.js.map +0 -1
- package/dist/chunk-MD2U6S4N.js.map +0 -1
- package/dist/chunk-TGFNQBKC.js.map +0 -1
- package/dist/chunk-V3QINEEG.js.map +0 -1
- package/dist/chunk-W6WQ5UP7.js.map +0 -1
- package/dist/session-YCQS4MRH.js +0 -9
package/dist/hotspots/index.js
CHANGED
|
@@ -1,11 +1,125 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
23
|
-
|
|
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(
|
|
27
|
-
return [
|
|
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.
|
|
37
|
-
|
|
235
|
+
size = sizing === "screen" ? 0.015 : 0.03,
|
|
236
|
+
showLabel = "never",
|
|
237
|
+
labelOpacity = 0.9,
|
|
238
|
+
labelSize = 1
|
|
38
239
|
} = options;
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
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(
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
440
|
+
opacity: 0,
|
|
61
441
|
depthWrite: false
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 ||
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
camera.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 ||
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
805
|
+
core,
|
|
806
|
+
orb: core,
|
|
155
807
|
halo,
|
|
808
|
+
ring,
|
|
809
|
+
shell,
|
|
810
|
+
label: labelMesh,
|
|
156
811
|
rig,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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 {
|
|
192
|
-
|
|
193
|
-
|
|
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 &&
|
|
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
|
-
|
|
1108
|
+
hovered?.setHovered(false);
|
|
217
1109
|
hovered = next;
|
|
218
|
-
|
|
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) =>
|
|
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
|