@scenography/engine 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,220 +1,374 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let three = require("three");
3
- //#region src/controls/boundaries.ts
4
- /**
5
- * Clamp a step on the XZ plane to a square `[-bound, bound]`. Axes are clamped
6
- * independently so sliding along a wall still works when one axis is blocked.
7
- */
8
- const clampToBounds = (current, step, bound) => {
9
- const next = new three.Vector3().addVectors(current, step);
10
- if (next.x < -bound || next.x > bound) next.x = current.x;
11
- if (next.z < -bound || next.z > bound) next.z = current.z;
12
- return next;
13
- };
14
- //#endregion
15
- //#region src/controls/headBob.ts
16
- const createHeadBobState = () => ({
17
- active: false,
18
- timer: 0
19
- });
20
- /**
21
- * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength
22
- * steps so the bob always settles back to neutral when movement stops.
23
- */
24
- const updateHeadBob = (state, elapsed) => {
25
- if (!state.active) return;
26
- const wavelength = Math.PI;
27
- const nextStepTime = (1 + Math.floor((state.timer + 1e-6) * 10 / wavelength)) * wavelength / 10;
28
- state.timer = Math.min(state.timer + elapsed, nextStepTime);
29
- if (state.timer === nextStepTime) state.active = false;
30
- };
31
- /** Vertical offset to add to the camera, in world units. */
32
- const headBobOffset = (state) => Math.sin(state.timer * 10) * 1.5;
33
- //#endregion
34
- //#region src/controls/keyboard.ts
35
- const createKeyboardState = () => ({});
36
- const onKeyDown = (state, event) => {
37
- state[event.code] = true;
38
- };
39
- const onKeyUp = (state, event) => {
40
- state[event.code] = false;
41
- };
42
- /**
43
- * Compute the per-frame translation from WASD input, oriented by the current
44
- * yaw (`phi`). Holding Shift acts as a speed booster.
45
- */
46
- const movementFromKeyboard = (state, phi, delta) => {
47
- const has = (code) => state[code] ? 1 : 0;
48
- const forwardVelocity = has("KeyW") - has("KeyS");
49
- const strafeVelocity = has("KeyA") - has("KeyD");
50
- const factor = state["ShiftLeft"] || state["ShiftRight"] ? 30 : 10;
51
- const yaw = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), phi);
52
- const forward = new three.Vector3(0, 0, -10).applyQuaternion(yaw).multiplyScalar(forwardVelocity * delta * factor);
53
- const left = new three.Vector3(-10, 0, 0).applyQuaternion(yaw).multiplyScalar(strafeVelocity * delta * factor);
54
- return forward.add(left);
55
- };
56
- //#endregion
57
- //#region src/controls/mouse.ts
58
- const PHI_SPEED = 8;
59
- const THETA_SPEED = 5;
60
- const createMouseState = () => ({
61
- dragging: false,
62
- pos: new three.Vector2(),
63
- prevPos: new three.Vector2(),
64
- delta: new three.Vector2()
65
- });
66
- const clamp = (x, a, b) => Math.min(Math.max(x, a), b);
67
- const onMouseDown = (state, event) => {
68
- state.dragging = event.button === 0;
69
- };
70
- const onMouseUp = (state) => {
71
- state.dragging = false;
72
- };
73
- const onMouseMove = (state, event) => {
74
- state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);
75
- state.delta.subVectors(state.pos, state.prevPos);
76
- };
77
- const commitMouseFrame = (state) => {
78
- state.prevPos.copy(state.pos);
79
- state.delta.set(0, 0);
3
+ //#region src/environment/EnvironmentController.ts
4
+ /** Load an equirectangular HDRI, picking the loader by extension. */
5
+ const loadHDRI = (src, onLoad) => {
6
+ if (src.toLowerCase().endsWith(".exr")) import("three/examples/jsm/loaders/EXRLoader.js").then(({ EXRLoader }) => {
7
+ new EXRLoader().load(src, onLoad);
8
+ });
9
+ else import("three/examples/jsm/loaders/RGBELoader.js").then(({ RGBELoader }) => {
10
+ new RGBELoader().load(src, onLoad);
11
+ });
80
12
  };
81
13
  /**
82
- * Integrate the pointer delta into a look quaternion. Mutates `look` and
83
- * returns the resulting rotation. Yaw is unbounded; pitch is clamped to avoid
84
- * flipping over.
14
+ * Owns the scene's HDRI sky / image-based lighting and keeps it in sync with the
15
+ * `Environment` definition. Loads the equirectangular map on demand (its loader
16
+ * ships in its own chunk) and assigns it to `scene.background` and/or
17
+ * `scene.environment`. Reconciles by a JSON hash so hot edits only reload when
18
+ * the definition actually changes.
85
19
  */
86
- const updateLookFromMouse = (mouse, look) => {
87
- const xh = mouse.delta.x / window.innerWidth;
88
- const yh = mouse.delta.y / window.innerHeight;
89
- look.phi += -xh * PHI_SPEED;
90
- look.theta = clamp(look.theta + -yh * THETA_SPEED, -Math.PI / 3, Math.PI / 3);
91
- const qy = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), look.phi);
92
- const qx = new three.Quaternion().setFromAxisAngle(new three.Vector3(1, 0, 0), look.theta);
93
- return new three.Quaternion().multiply(qy).multiply(qx);
20
+ var EnvironmentController = class {
21
+ scene;
22
+ hash = null;
23
+ texture = null;
24
+ constructor(scene) {
25
+ this.scene = scene;
26
+ }
27
+ reconcile(env) {
28
+ const hash = env ? JSON.stringify(env) : null;
29
+ if (hash === this.hash) return;
30
+ this.hash = hash;
31
+ this.clear();
32
+ if (!env) return;
33
+ loadHDRI(env.src, (tex) => {
34
+ if (this.hash !== hash) {
35
+ tex.dispose();
36
+ return;
37
+ }
38
+ tex.mapping = three.EquirectangularReflectionMapping;
39
+ this.texture = tex;
40
+ if (env.background ?? true) {
41
+ this.scene.background = tex;
42
+ this.scene.backgroundIntensity = env.intensity ?? 1;
43
+ }
44
+ if (env.lighting ?? true) this.scene.environment = tex;
45
+ });
46
+ }
47
+ /** Set the visible sky background brightness (0 = black). Live, for behavior. */
48
+ setBackgroundIntensity(value) {
49
+ this.scene.backgroundIntensity = value;
50
+ }
51
+ clear() {
52
+ this.scene.background = null;
53
+ this.scene.environment = null;
54
+ this.texture?.dispose();
55
+ this.texture = null;
56
+ }
57
+ dispose() {
58
+ this.clear();
59
+ this.hash = null;
60
+ }
94
61
  };
95
62
  //#endregion
96
- //#region src/controls/FreeRoamControls.ts
63
+ //#region src/interaction/InteractionController.ts
64
+ const HIGHLIGHT_COLOR = "#ef4444";
65
+ const HIGHLIGHT_THICKNESS = 1.5;
66
+ const HIGHLIGHT_MARGIN = 1.5;
67
+ const CLICK_SLOP = 4;
97
68
  /**
98
- * First-person, free-roaming camera controls ported from the original Virtual
99
- * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while
100
- * moving, and square boundary clamping. The cardboard / device-orientation path
101
- * was intentionally dropped.
102
- *
103
- * Framework-agnostic: construct it with a camera and the canvas element, call
104
- * `attach()`, then `update(delta)` once per frame.
69
+ * Pointer picking for artworks: hovering a piece's click target (its label, or
70
+ * the piece itself when it has none) draws a pulsing red frame and shows a
71
+ * pointer cursor; clicking it emits `artwork:select { id }`. Targets are marked
72
+ * in the scene graph (`userData.pickTarget`/`pickInfo`) by the artwork builder,
73
+ * so this just raycasts and walks up to find them. An optional per-id radius
74
+ * gates interaction to when the camera is reasonably close.
105
75
  */
106
- var FreeRoamControls = class {
107
- bounds;
76
+ var InteractionController = class {
108
77
  camera;
109
- domElement;
110
- mouse = createMouseState();
111
- keyboard = createKeyboardState();
112
- headBob = createHeadBobState();
113
- look = {
114
- phi: 0,
115
- theta: 0
116
- };
117
- rotation = new three.Quaternion();
118
- translation = new three.Vector3(0, 170, 0);
78
+ canvas;
79
+ scene;
80
+ emit;
81
+ canInteract;
82
+ raycaster = new three.Raycaster();
83
+ pointer = new three.Vector2();
84
+ over = false;
85
+ downPos = null;
86
+ radii = /* @__PURE__ */ new Map();
87
+ hovered = null;
88
+ time = 0;
89
+ highlight = new three.Group();
90
+ highlightMat = null;
119
91
  handlers = [];
120
- constructor(camera, domElement, options = {}) {
92
+ constructor(camera, canvas, scene, emit, canInteract) {
121
93
  this.camera = camera;
122
- this.domElement = domElement;
123
- this.bounds = options.bounds ?? 450;
124
- if (options.position) this.translation.copy(options.position);
125
- if (options.yaw !== void 0) this.look.phi = options.yaw;
126
- this.rotation.setFromAxisAngle(new three.Vector3(0, 1, 0), this.look.phi);
94
+ this.canvas = canvas;
95
+ this.scene = scene;
96
+ this.emit = emit;
97
+ this.canInteract = canInteract;
98
+ this.highlight.visible = false;
99
+ this.scene.add(this.highlight);
100
+ }
101
+ /** Per-artwork max interaction distance (XZ). Absent id = no distance gate. */
102
+ setRadii(radii) {
103
+ this.radii = radii;
127
104
  }
128
105
  attach() {
129
- const bind = (target, type, listener) => {
130
- target.addEventListener(type, listener);
106
+ const bind = (type, listener) => {
107
+ this.canvas.addEventListener(type, listener);
131
108
  this.handlers.push([
132
- target,
109
+ this.canvas,
133
110
  type,
134
111
  listener
135
112
  ]);
136
113
  };
137
- bind(this.domElement, "mousedown", (e) => onMouseDown(this.mouse, e));
138
- bind(window, "mousemove", (e) => onMouseMove(this.mouse, e));
139
- bind(window, "mouseup", () => onMouseUp(this.mouse));
140
- bind(window, "keydown", (e) => onKeyDown(this.keyboard, e));
141
- bind(window, "keyup", (e) => onKeyUp(this.keyboard, e));
114
+ bind("pointermove", (e) => this.onMove(e));
115
+ bind("pointerdown", (e) => this.onDown(e));
116
+ bind("pointerup", (e) => this.onUp(e));
117
+ bind("pointerleave", () => {
118
+ this.over = false;
119
+ });
142
120
  }
143
121
  detach() {
144
122
  for (const [target, type, listener] of this.handlers) target.removeEventListener(type, listener);
145
123
  this.handlers.length = 0;
124
+ this.scene.remove(this.highlight);
125
+ this.clearHighlight();
146
126
  }
147
- /** Advance the camera by `delta` seconds of input. */
148
- update(delta) {
149
- if (this.mouse.dragging) this.rotation = updateLookFromMouse(this.mouse, this.look);
150
- const step = movementFromKeyboard(this.keyboard, this.look.phi, delta);
151
- this.translation = clampToBounds(this.translation, step, this.bounds);
152
- if (step.x !== 0 || step.z !== 0) this.headBob.active = true;
153
- updateHeadBob(this.headBob, delta);
154
- this.camera.quaternion.copy(this.rotation);
155
- this.camera.position.copy(this.translation);
156
- this.camera.position.y += headBobOffset(this.headBob);
157
- const target = new three.Vector3(0, 0, -1).applyQuaternion(this.rotation).multiplyScalar(100).add(this.translation);
158
- this.camera.lookAt(target);
159
- commitMouseFrame(this.mouse);
127
+ setPointer(e) {
128
+ const rect = this.canvas.getBoundingClientRect();
129
+ this.pointer.set((e.clientX - rect.left) / rect.width * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
130
+ this.over = true;
131
+ }
132
+ onMove(e) {
133
+ this.setPointer(e);
134
+ }
135
+ onDown(e) {
136
+ this.downPos = {
137
+ x: e.clientX,
138
+ y: e.clientY
139
+ };
140
+ }
141
+ onUp(e) {
142
+ const down = this.downPos;
143
+ this.downPos = null;
144
+ if (!down || !this.canInteract()) return;
145
+ if (Math.hypot(e.clientX - down.x, e.clientY - down.y) > CLICK_SLOP) return;
146
+ this.setPointer(e);
147
+ const pick = this.raycast();
148
+ if (pick) this.emit("artwork:select", { id: pick.id });
149
+ }
150
+ /** Resolve the nearest pickable artwork under the pointer, honoring the gate. */
151
+ raycast() {
152
+ this.raycaster.setFromCamera(this.pointer, this.camera);
153
+ const hits = this.raycaster.intersectObjects(this.scene.children, true);
154
+ for (const hit of hits) {
155
+ let node = hit.object;
156
+ while (node && node.userData.pickTarget === void 0) node = node.parent;
157
+ if (!node) continue;
158
+ const id = node.userData.pickTarget;
159
+ const radius = this.radii.get(id);
160
+ if (radius !== void 0) {
161
+ const p = node.getWorldPosition(new three.Vector3());
162
+ if (Math.hypot(this.camera.position.x - p.x, this.camera.position.z - p.z) > radius) continue;
163
+ }
164
+ return {
165
+ id,
166
+ holder: node,
167
+ info: node.userData.pickInfo
168
+ };
169
+ }
170
+ return null;
171
+ }
172
+ /** Recompute hover state and animate the highlight. Call once per frame. */
173
+ update(deltaMs) {
174
+ this.time += deltaMs;
175
+ const pick = this.over && this.canInteract() ? this.raycast() : null;
176
+ if ((pick?.id ?? null) !== this.hovered) {
177
+ this.hovered = pick?.id ?? null;
178
+ this.canvas.style.cursor = pick ? "pointer" : "";
179
+ if (pick) this.buildHighlight(pick);
180
+ else this.highlight.visible = false;
181
+ }
182
+ if (this.highlight.visible && this.highlightMat) this.highlightMat.opacity = .45 + .45 * (.5 + .5 * Math.sin(this.time / 320));
183
+ }
184
+ clearHighlight() {
185
+ for (const child of [...this.highlight.children]) {
186
+ this.highlight.remove(child);
187
+ child.geometry?.dispose();
188
+ }
189
+ this.highlightMat?.dispose();
190
+ this.highlightMat = null;
191
+ }
192
+ /** Position a fresh pulsing red frame over the picked target's front face. */
193
+ buildHighlight(pick) {
194
+ this.clearHighlight();
195
+ const mat = new three.MeshBasicMaterial({
196
+ color: HIGHLIGHT_COLOR,
197
+ transparent: true,
198
+ depthTest: false
199
+ });
200
+ this.highlightMat = mat;
201
+ const w = pick.info.width + 2 * HIGHLIGHT_MARGIN;
202
+ const h = pick.info.height + 2 * HIGHLIGHT_MARGIN;
203
+ const t = HIGHLIGHT_THICKNESS;
204
+ const bars = [
205
+ [
206
+ w + 2 * t,
207
+ t,
208
+ 0,
209
+ h / 2 + t / 2
210
+ ],
211
+ [
212
+ w + 2 * t,
213
+ t,
214
+ 0,
215
+ -(h / 2 + t / 2)
216
+ ],
217
+ [
218
+ t,
219
+ h,
220
+ -(w / 2 + t / 2),
221
+ 0
222
+ ],
223
+ [
224
+ t,
225
+ h,
226
+ w / 2 + t / 2,
227
+ 0
228
+ ]
229
+ ];
230
+ for (const [bw, bh, x, y] of bars) {
231
+ const bar = new three.Mesh(new three.BoxGeometry(bw, bh, .5), mat);
232
+ bar.position.set(x, y, 0);
233
+ bar.renderOrder = 999;
234
+ this.highlight.add(bar);
235
+ }
236
+ const pos = pick.holder.getWorldPosition(new three.Vector3());
237
+ const quat = pick.holder.getWorldQuaternion(new three.Quaternion());
238
+ const normal = new three.Vector3(0, 0, 1).applyQuaternion(quat);
239
+ pos.addScaledVector(normal, pick.info.frontOffset);
240
+ this.highlight.position.copy(pos);
241
+ this.highlight.quaternion.copy(quat);
242
+ this.highlight.visible = true;
160
243
  }
161
244
  };
162
245
  //#endregion
163
- //#region src/builders/wall.ts
164
- /** Build the three.js object that materializes a `Wall` definition. */
165
- function buildWall(wall) {
166
- const [width, height] = wall.size;
167
- const mesh = new three.Mesh(new three.PlaneGeometry(width, height), new three.MeshStandardMaterial({
168
- color: wall.color ?? "#ffffff",
169
- side: three.DoubleSide
170
- }));
171
- mesh.position.set(wall.position[0], wall.position[1], wall.position[2]);
172
- mesh.rotation.y = wall.rotation ?? 0;
173
- mesh.name = `wall:${wall.id}`;
174
- mesh.userData.scenographyId = wall.id;
175
- return mesh;
176
- }
246
+ //#region src/input/InputBus.ts
247
+ /**
248
+ * The shared analog-input channel: the current movement and look intent, each
249
+ * axis in `-1..1` and held until a producer changes it. Producers (on-screen
250
+ * joysticks in the overlay, a gamepad…) write to it; whatever camera system is
251
+ * active reads it and interprets it its own way — free-roam walks with it, a
252
+ * fixed camera might only use the look axes to glance around. This is what makes
253
+ * the joysticks reusable across camera types. Pure state — no three.js.
254
+ */
255
+ var InputBus = class {
256
+ /** Forward (`+1`) / backward (`-1`). */
257
+ moveForward = 0;
258
+ /** Strafe right (`+1`) / left (`-1`). */
259
+ moveStrafe = 0;
260
+ /** Look right (`+1`) / left (`-1`). */
261
+ lookX = 0;
262
+ /** Look up (`+1`) / down (`-1`). */
263
+ lookY = 0;
264
+ /** Set the movement intent (forward, strafe). */
265
+ setMove(forward, strafe = 0) {
266
+ this.moveForward = forward;
267
+ this.moveStrafe = strafe;
268
+ }
269
+ /** Set the look intent (turn, pitch). */
270
+ setLook(x, y = 0) {
271
+ this.lookX = x;
272
+ this.lookY = y;
273
+ }
274
+ };
177
275
  //#endregion
178
- //#region src/builders/artwork.ts
179
- const loader = new three.TextureLoader();
180
- const FRAME_MARGIN = 4;
181
- const SURFACE_OFFSET = 1;
276
+ //#region src/plugin/CapabilityRegistry.ts
182
277
  /**
183
- * Build the three.js object for an `Artwork`, placing it on its wall. The piece
184
- * is positioned in the wall's local plane via `offset` and nudged forward along
185
- * the wall normal so it sits in front of the surface.
278
+ * Shared runtime contracts between plugins, keyed by name. A plugin (or the
279
+ * kernel) *provides* a capability — e.g. the floor provides `terrain` — and
280
+ * other plugins *consume* it if present, without depending on the provider's
281
+ * package. Each name keeps a monotonic version counter so consumers can hash
282
+ * their dependencies cheaply and rebuild when a provider changes.
186
283
  */
187
- function buildArtwork(artwork, wall) {
188
- const [width, height] = artwork.size;
189
- const [offsetX, offsetY] = artwork.offset ?? [0, 0];
190
- const yaw = wall.rotation ?? 0;
191
- const group = new three.Group();
192
- const wallQuat = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), yaw);
193
- const right = new three.Vector3(1, 0, 0).applyQuaternion(wallQuat);
194
- const normal = new three.Vector3(0, 0, 1).applyQuaternion(wallQuat);
195
- const position = new three.Vector3(wall.position[0], wall.position[1], wall.position[2]).add(right.clone().multiplyScalar(offsetX)).add(new three.Vector3(0, offsetY, 0)).add(normal.clone().multiplyScalar(SURFACE_OFFSET));
196
- group.position.copy(position);
197
- group.rotation.y = yaw;
198
- const frame = new three.Mesh(new three.PlaneGeometry(width + FRAME_MARGIN, height + FRAME_MARGIN), new three.MeshStandardMaterial({ color: "#1a1a1a" }));
199
- group.add(frame);
200
- const texture = loader.load(artwork.src);
201
- texture.colorSpace = three.SRGBColorSpace;
202
- const image = new three.Mesh(new three.PlaneGeometry(width, height), new three.MeshBasicMaterial({ map: texture }));
203
- image.position.z = .1;
204
- group.add(image);
205
- group.name = `artwork:${artwork.id}`;
206
- group.userData.scenographyId = artwork.id;
207
- group.userData.wallId = artwork.wall;
208
- return group;
209
- }
284
+ var CapabilityRegistry = class {
285
+ values = /* @__PURE__ */ new Map();
286
+ versions = /* @__PURE__ */ new Map();
287
+ /** Last change-token seen per name, to suppress redundant version bumps. */
288
+ tokens = /* @__PURE__ */ new Map();
289
+ /**
290
+ * Publish (or replace) the capability under `name`. Pass a `token` (a cheap
291
+ * hash of the inputs) to dedupe: re-providing with an unchanged token is a
292
+ * no-op, so consumers that depend on this capability don't rebuild when the
293
+ * provider recomputes an identical value.
294
+ */
295
+ provide(name, value, token) {
296
+ if (token !== void 0 && this.values.has(name) && this.tokens.get(name) === token) return;
297
+ this.values.set(name, value);
298
+ if (token !== void 0) this.tokens.set(name, token);
299
+ else this.tokens.delete(name);
300
+ this.bump(name);
301
+ }
302
+ /** Remove the capability; consumers see `undefined` and a version bump. */
303
+ revoke(name) {
304
+ if (this.values.delete(name)) {
305
+ this.tokens.delete(name);
306
+ this.bump(name);
307
+ }
308
+ }
309
+ get(name) {
310
+ return this.values.get(name);
311
+ }
312
+ /** Monotonic change counter for `name` (`0` if never provided). */
313
+ version(name) {
314
+ return this.versions.get(name) ?? 0;
315
+ }
316
+ bump(name) {
317
+ this.versions.set(name, (this.versions.get(name) ?? 0) + 1);
318
+ }
319
+ };
320
+ //#endregion
321
+ //#region src/plugin/systemPlugin.ts
322
+ /** Narrow a mixed plugin to a system plugin (has `setup`, not `build`). */
323
+ const isSystemPlugin = (plugin) => typeof plugin === "object" && plugin !== null && "setup" in plugin;
324
+ //#endregion
325
+ //#region src/plugin/roamControl.ts
326
+ /** Capability name under which a roaming camera system exposes pose control. */
327
+ const ROAM_CONTROL = "roamControl";
328
+ //#endregion
329
+ //#region src/plugin/terrain.ts
330
+ /** Capability name under which the floor publishes its terrain. */
331
+ const TERRAIN = "terrain";
332
+ //#endregion
333
+ //#region src/plugin/colliders.ts
334
+ /** Capability name under which walkable-space obstacles are published. */
335
+ const COLLIDERS = "colliders";
336
+ //#endregion
337
+ //#region src/plugin/edges.ts
338
+ /** Capability name under which named wall edges are published for hanging. */
339
+ const EDGES = "edges";
340
+ //#endregion
341
+ //#region src/plugin/eyeHeight.ts
342
+ /**
343
+ * Capability name under which the visitor's eye height (world units) is
344
+ * published — the default vertical placement for wall-hung artworks. The kernel
345
+ * provides it from the camera start; a camera plugin will own it later.
346
+ */
347
+ const EYE_HEIGHT = "eyeHeight";
210
348
  //#endregion
211
349
  //#region src/SceneManager.ts
212
350
  const disposeObject = (object) => {
213
351
  object.traverse((child) => {
352
+ const video = child.userData?.video;
353
+ if (video) {
354
+ video.pause();
355
+ video.removeAttribute("src");
356
+ video.load();
357
+ }
214
358
  const mesh = child;
215
359
  mesh.geometry?.dispose?.();
360
+ const dispose = (m) => {
361
+ const material = m;
362
+ for (const slot of [
363
+ "map",
364
+ "normalMap",
365
+ "roughnessMap",
366
+ "aoMap",
367
+ "metalnessMap"
368
+ ]) (material?.[slot])?.dispose?.();
369
+ material?.dispose?.();
370
+ };
216
371
  const material = mesh.material;
217
- const dispose = (m) => m?.dispose?.();
218
372
  if (Array.isArray(material)) material.forEach(dispose);
219
373
  else dispose(material);
220
374
  });
@@ -228,82 +382,109 @@ const disposeObject = (object) => {
228
382
  */
229
383
  var SceneManager = class {
230
384
  root;
231
- walls = /* @__PURE__ */ new Map();
232
- artworks = /* @__PURE__ */ new Map();
233
- constructor(root) {
385
+ plugins;
386
+ capabilities;
387
+ /** Mounted plugin elements, per kind, keyed by element id ("" for singletons). */
388
+ pluginElements = /* @__PURE__ */ new Map();
389
+ constructor(root, plugins = [], capabilities = new CapabilityRegistry()) {
234
390
  this.root = root;
391
+ this.plugins = plugins;
392
+ this.capabilities = capabilities;
235
393
  }
236
394
  reconcile(scene) {
237
- const wallById = new Map(scene.walls.map((w) => [w.id, w]));
238
- this.reconcileWalls(scene.walls);
239
- this.reconcileArtworks(scene.artworks, wallById);
395
+ const eye = scene.camera.start.position[1];
396
+ this.capabilities.provide(EYE_HEIGHT, eye, String(eye));
397
+ this.reconcilePlugins(scene);
240
398
  }
241
- reconcileWalls(walls) {
242
- const next = new Set(walls.map((w) => w.id));
243
- for (const [id, entry] of this.walls) if (!next.has(id)) {
244
- this.root.remove(entry.object);
245
- disposeObject(entry.object);
246
- this.walls.delete(id);
247
- }
248
- for (const wall of walls) {
249
- const hash = JSON.stringify(wall);
250
- const existing = this.walls.get(wall.id);
251
- if (existing?.hash === hash) continue;
252
- if (existing) {
253
- this.root.remove(existing.object);
254
- disposeObject(existing.object);
399
+ /**
400
+ * Reconcile every registered plugin's elements by content: an element
401
+ * rebuilds when its definition changes or when a capability it depends on
402
+ * was re-provided (the capability versions are part of the hash).
403
+ */
404
+ reconcilePlugins(scene) {
405
+ for (const plugin of this.plugins) {
406
+ const { kind, cardinality } = plugin.data;
407
+ let mounted = this.pluginElements.get(kind);
408
+ if (!mounted) {
409
+ mounted = /* @__PURE__ */ new Map();
410
+ this.pluginElements.set(kind, mounted);
411
+ }
412
+ const raw = scene[kind];
413
+ const items = cardinality === "many" ? Array.isArray(raw) ? raw.map((item, i) => [String(item.id ?? i), item]) : [] : raw !== void 0 ? [["", raw]] : [];
414
+ const deps = (plugin.dependsOn ?? []).map((name) => this.capabilities.version(name));
415
+ const seen = /* @__PURE__ */ new Set();
416
+ for (const [key, value] of items) {
417
+ seen.add(key);
418
+ const hash = JSON.stringify({
419
+ value,
420
+ deps
421
+ });
422
+ const existing = mounted.get(key);
423
+ if (existing?.hash === hash) continue;
424
+ if (existing) this.removePluginElement(mounted, key, existing);
425
+ const built = plugin.build(value, { capabilities: this.capabilities });
426
+ if (built.object) this.root.add(built.object);
427
+ mounted.set(key, {
428
+ ...built,
429
+ hash
430
+ });
255
431
  }
256
- const object = buildWall(wall);
257
- this.root.add(object);
258
- this.walls.set(wall.id, {
259
- object,
260
- hash
261
- });
432
+ for (const [key, entry] of mounted) if (!seen.has(key)) this.removePluginElement(mounted, key, entry);
433
+ plugin.provide?.(items.map(([, value]) => value), { capabilities: this.capabilities });
262
434
  }
263
435
  }
264
- reconcileArtworks(artworks, wallById) {
265
- const next = new Set(artworks.map((a) => a.id));
266
- for (const [id, entry] of this.artworks) if (!next.has(id)) {
436
+ removePluginElement(mounted, key, entry) {
437
+ if (entry.object) {
267
438
  this.root.remove(entry.object);
268
439
  disposeObject(entry.object);
269
- this.artworks.delete(id);
270
- }
271
- for (const artwork of artworks) {
272
- const wall = wallById.get(artwork.wall);
273
- if (!wall) continue;
274
- const hash = JSON.stringify({
275
- a: artwork,
276
- w: wall
277
- });
278
- const existing = this.artworks.get(artwork.id);
279
- if (existing?.hash === hash) continue;
280
- if (existing) {
281
- this.root.remove(existing.object);
282
- disposeObject(existing.object);
283
- }
284
- const object = buildArtwork(artwork, wall);
285
- this.root.add(object);
286
- this.artworks.set(artwork.id, {
287
- object,
288
- hash
289
- });
290
440
  }
441
+ entry.dispose?.();
442
+ mounted.delete(key);
443
+ }
444
+ /** Advance time-based plugin elements (e.g. water ripple). Call once per frame. */
445
+ updateElements(deltaMs) {
446
+ for (const mounted of this.pluginElements.values()) for (const entry of mounted.values()) entry.update?.(deltaMs);
291
447
  }
292
- /** Look up the live three.js object for a wall or artwork `id`. */
448
+ /** Run every element's auxiliary pre-render pass (e.g. water reflection). */
449
+ beforeRender(renderer, scene, camera) {
450
+ for (const mounted of this.pluginElements.values()) for (const entry of mounted.values()) entry.beforeRender?.(renderer, scene, camera);
451
+ }
452
+ /** Look up the live three.js object for any plugin element `id`. */
293
453
  get(id) {
294
- return this.walls.get(id)?.object ?? this.artworks.get(id)?.object;
454
+ for (const mounted of this.pluginElements.values()) {
455
+ const object = mounted.get(id)?.object;
456
+ if (object) return object;
457
+ }
458
+ }
459
+ /** Live objects of one plugin kind, keyed by `id` — for that plugin's controller. */
460
+ elementsOf(kind) {
461
+ const out = /* @__PURE__ */ new Map();
462
+ const mounted = this.pluginElements.get(kind);
463
+ if (mounted) {
464
+ for (const [id, entry] of mounted) if (entry.object) out.set(id, entry.object);
465
+ }
466
+ return out;
295
467
  }
296
468
  dispose() {
297
- for (const entry of [...this.walls.values(), ...this.artworks.values()]) {
298
- this.root.remove(entry.object);
299
- disposeObject(entry.object);
469
+ for (const [kind, mounted] of this.pluginElements) {
470
+ for (const [key, entry] of mounted) this.removePluginElement(mounted, key, entry);
471
+ this.pluginElements.delete(kind);
300
472
  }
301
- this.walls.clear();
302
- this.artworks.clear();
303
473
  }
304
474
  };
305
475
  //#endregion
306
476
  //#region src/createGallery.ts
477
+ /** Maps a plugin kind/name onto the `ctx` slot its api fills. */
478
+ const CTX_SLOT = {
479
+ camera: "focus",
480
+ lights: "lights",
481
+ video: "videos",
482
+ sky: "sky",
483
+ media: "audio"
484
+ };
485
+ /** The system the cardboard render toggle drives (gyroscope head-look). */
486
+ const CARDBOARD_SYSTEM = "cardboard";
487
+ const DEFAULT_EYE_SEPARATION = 3;
307
488
  /**
308
489
  * Create and run a gallery from a scene definition. Sets up the three.js
309
490
  * renderer, camera, lights and free-roam controls, materializes the scene, and
@@ -315,35 +496,220 @@ function createGallery(scene, options) {
315
496
  const height = canvas.clientHeight || window.innerHeight;
316
497
  const renderer = new three.WebGLRenderer({
317
498
  canvas,
318
- antialias: options.antialias ?? true
499
+ antialias: false
319
500
  });
320
501
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
321
502
  renderer.setSize(width, height, false);
503
+ renderer.shadowMap.type = three.PCFShadowMap;
504
+ const render = {
505
+ antialias: options.antialias ?? 4,
506
+ ssao: options.ssao ?? false,
507
+ bloom: options.bloom ?? 0,
508
+ shadows: options.shadows ?? true,
509
+ cardboard: options.cardboard ?? false,
510
+ eyeSeparation: options.eyeSeparation ?? DEFAULT_EYE_SEPARATION
511
+ };
512
+ const stereo = new three.StereoCamera();
513
+ stereo.aspect = .5;
514
+ stereo.eyeSep = render.eyeSeparation;
515
+ const stereoSize = new three.Vector2();
322
516
  const threeScene = new three.Scene();
323
- const camera = new three.PerspectiveCamera(75, width / height, .1, 5e3);
324
- const { position, yaw } = scene.camera.start;
517
+ const camera = new three.PerspectiveCamera(scene.camera.fov ?? 55, width / height, .1, 5e3);
518
+ const { position } = scene.camera.start;
325
519
  camera.position.set(position[0], position[1], position[2]);
326
- threeScene.add(new three.AmbientLight(16777215, 1.4));
327
- threeScene.add(new three.HemisphereLight(16777215, 4473924, 1));
328
- const manager = new SceneManager(threeScene);
520
+ const allPlugins = options.plugins ?? [];
521
+ const systemPlugins = allPlugins.filter(isSystemPlugin);
522
+ const elementPlugins = allPlugins.filter((p) => !isSystemPlugin(p));
523
+ const capabilities = new CapabilityRegistry();
524
+ const manager = new SceneManager(threeScene, elementPlugins, capabilities);
329
525
  manager.reconcile(scene);
330
- const controls = new FreeRoamControls(camera, canvas, {
331
- bounds: scene.camera.bounds,
332
- position: camera.position.clone(),
333
- yaw
334
- });
335
- controls.attach();
526
+ const input = new InputBus();
527
+ const environment = new EnvironmentController(threeScene);
528
+ environment.reconcile(scene.environment);
336
529
  const listeners = /* @__PURE__ */ new Map();
530
+ const emit = (event, payload) => {
531
+ listeners.get(event)?.forEach((handler) => handler(payload));
532
+ };
533
+ const frameHandlers = /* @__PURE__ */ new Set();
534
+ const apis = /* @__PURE__ */ new Map();
535
+ const controllers = [];
536
+ for (const plugin of elementPlugins) {
537
+ const controller = plugin.controller?.(() => manager.elementsOf(plugin.data.kind));
538
+ if (!controller) continue;
539
+ if (controller.api !== void 0) apis.set(plugin.data.kind, controller.api);
540
+ controllers.push(controller);
541
+ }
542
+ const systems = systemPlugins.map((plugin) => {
543
+ const instance = plugin.setup({
544
+ scene,
545
+ camera,
546
+ canvas,
547
+ scene3d: threeScene,
548
+ renderer,
549
+ input,
550
+ capabilities,
551
+ emit,
552
+ resolve: (id) => manager.get(id)
553
+ });
554
+ if (instance.api !== void 0) apis.set(plugin.name, instance.api);
555
+ return instance;
556
+ });
557
+ const cameraApi = apis.get(CTX_SLOT.camera) ?? {
558
+ focusArtwork: () => {},
559
+ centerArtwork: () => {},
560
+ release: () => {},
561
+ isAnimating: () => false
562
+ };
563
+ const headTracking = apis.get(CARDBOARD_SYSTEM);
564
+ if (options.cardboard) headTracking?.setEnabled(true);
565
+ const lightsApi = apis.get(CTX_SLOT.lights) ?? {
566
+ set: () => {},
567
+ dimAll: () => {},
568
+ restore: () => {}
569
+ };
570
+ const videoApi = apis.get(CTX_SLOT.video) ?? {
571
+ play: () => {},
572
+ pause: () => {},
573
+ setVisible: () => {}
574
+ };
575
+ const skyApi = apis.get(CTX_SLOT.sky) ?? {
576
+ setTime: () => {},
577
+ getTime: () => 12,
578
+ setAzimuth: () => {},
579
+ getAzimuth: () => 0
580
+ };
581
+ const mediaApi = apis.get(CTX_SLOT.media) ?? {
582
+ play: () => {},
583
+ pause: () => {},
584
+ stop: () => {},
585
+ setVolume: () => {},
586
+ playing: () => false,
587
+ setMuted: () => {},
588
+ muted: () => false
589
+ };
590
+ const radiiOf = (s) => {
591
+ const gated = s.artworks ?? [];
592
+ return new Map(gated.filter((a) => a.infoRadius !== void 0).map((a) => [a.id, a.infoRadius]));
593
+ };
594
+ const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating());
595
+ interaction.setRadii(radiiOf(scene));
596
+ interaction.attach();
337
597
  const clock = new three.Clock();
338
598
  let rafId = null;
599
+ const BLOOM = {
600
+ radius: .4,
601
+ threshold: .85
602
+ };
603
+ let postClasses = null;
604
+ let composer = null;
605
+ let ssaoPass = null;
606
+ let bloomPass = null;
607
+ let composerLoading = false;
608
+ const pixelRatio = Math.min(window.devicePixelRatio, 2);
609
+ const needsComposer = () => render.antialias > 0 || render.ssao || render.bloom > 0;
610
+ const buildComposer = () => {
611
+ if (!postClasses) return;
612
+ composer?.dispose();
613
+ const { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass } = postClasses;
614
+ const c = new EffectComposer(renderer, new three.WebGLRenderTarget(1, 1, {
615
+ samples: render.antialias,
616
+ type: three.HalfFloatType
617
+ }));
618
+ c.setPixelRatio(pixelRatio);
619
+ c.setSize(width, height);
620
+ c.addPass(new RenderPass(threeScene, camera));
621
+ const ssao = new SSAOPass(threeScene, camera, width, height);
622
+ ssao.kernelRadius = 16;
623
+ ssao.enabled = render.ssao;
624
+ c.addPass(ssao);
625
+ const bloom = new UnrealBloomPass(new three.Vector2(width, height), render.bloom, BLOOM.radius, BLOOM.threshold);
626
+ bloom.enabled = render.bloom > 0;
627
+ c.addPass(bloom);
628
+ c.addPass(new OutputPass());
629
+ composer = c;
630
+ ssaoPass = ssao;
631
+ bloomPass = bloom;
632
+ };
633
+ const ensureComposer = () => {
634
+ if (!needsComposer()) return;
635
+ if (postClasses) {
636
+ if (!composer) buildComposer();
637
+ return;
638
+ }
639
+ if (composerLoading) return;
640
+ composerLoading = true;
641
+ Promise.all([
642
+ import("three/examples/jsm/postprocessing/EffectComposer.js"),
643
+ import("three/examples/jsm/postprocessing/RenderPass.js"),
644
+ import("three/examples/jsm/postprocessing/SSAOPass.js"),
645
+ import("three/examples/jsm/postprocessing/UnrealBloomPass.js"),
646
+ import("three/examples/jsm/postprocessing/OutputPass.js")
647
+ ]).then(([{ EffectComposer }, { RenderPass }, { SSAOPass }, { UnrealBloomPass }, { OutputPass }]) => {
648
+ postClasses = {
649
+ EffectComposer,
650
+ RenderPass,
651
+ SSAOPass,
652
+ UnrealBloomPass,
653
+ OutputPass
654
+ };
655
+ composerLoading = false;
656
+ buildComposer();
657
+ });
658
+ };
659
+ const refreshShadows = () => {
660
+ renderer.shadowMap.enabled = render.shadows;
661
+ threeScene.traverse((obj) => {
662
+ const mat = obj.material;
663
+ if (!mat) return;
664
+ for (const m of Array.isArray(mat) ? mat : [mat]) m.needsUpdate = true;
665
+ });
666
+ };
667
+ refreshShadows();
668
+ ensureComposer();
669
+ const renderStereo = () => {
670
+ camera.updateWorldMatrix(true, false);
671
+ stereo.update(camera);
672
+ renderer.getSize(stereoSize);
673
+ const w = stereoSize.x;
674
+ const h = stereoSize.y;
675
+ if (renderer.autoClear) renderer.clear();
676
+ renderer.setScissorTest(true);
677
+ renderer.setScissor(0, 0, w / 2, h);
678
+ renderer.setViewport(0, 0, w / 2, h);
679
+ renderer.render(threeScene, stereo.cameraL);
680
+ renderer.setScissor(w / 2, 0, w / 2, h);
681
+ renderer.setViewport(w / 2, 0, w / 2, h);
682
+ renderer.render(threeScene, stereo.cameraR);
683
+ renderer.setScissorTest(false);
684
+ renderer.setViewport(0, 0, w, h);
685
+ };
339
686
  const renderFrame = () => {
340
687
  rafId = requestAnimationFrame(renderFrame);
341
- controls.update(clock.getDelta());
342
- renderer.render(threeScene, camera);
688
+ const delta = clock.getDelta();
689
+ const deltaMs = delta * 1e3;
690
+ for (const system of systems) system.update?.(deltaMs);
691
+ interaction.update(deltaMs);
692
+ manager.updateElements(deltaMs);
693
+ for (const controller of controllers) controller.update?.(deltaMs);
694
+ for (const handler of frameHandlers) handler(deltaMs);
695
+ manager.beforeRender(renderer, threeScene, camera);
696
+ if (render.cardboard) renderStereo();
697
+ else if (composer && needsComposer()) composer.render(delta);
698
+ else renderer.render(threeScene, camera);
343
699
  };
344
700
  return {
345
701
  ctx: {
346
702
  entity: (id) => manager.get(id),
703
+ lights: lightsApi,
704
+ media: mediaApi,
705
+ controls: {
706
+ setMoveInput: (forward, strafe) => input.setMove(forward, strafe),
707
+ setLookInput: (x, y) => input.setLook(x, y)
708
+ },
709
+ environment: { setBackgroundIntensity: (v) => environment.setBackgroundIntensity(v) },
710
+ sky: skyApi,
711
+ camera: cameraApi,
712
+ video: videoApi,
347
713
  three: {
348
714
  scene: threeScene,
349
715
  camera,
@@ -352,7 +718,14 @@ function createGallery(scene, options) {
352
718
  },
353
719
  setScene(next) {
354
720
  manager.reconcile(next);
355
- if (next.camera.bounds !== void 0) controls.bounds = next.camera.bounds;
721
+ interaction.setRadii(radiiOf(next));
722
+ environment.reconcile(next.environment);
723
+ for (const system of systems) system.setScene?.(next);
724
+ const fov = next.camera.fov ?? 55;
725
+ if (camera.fov !== fov) {
726
+ camera.fov = fov;
727
+ camera.updateProjectionMatrix();
728
+ }
356
729
  },
357
730
  start() {
358
731
  if (rafId === null) {
@@ -368,10 +741,15 @@ function createGallery(scene, options) {
368
741
  },
369
742
  dispose() {
370
743
  this.stop();
371
- controls.detach();
744
+ for (const system of systems) system.dispose?.();
745
+ for (const controller of controllers) controller.dispose?.();
746
+ interaction.detach();
747
+ environment.dispose();
372
748
  manager.dispose();
749
+ composer?.dispose();
373
750
  renderer.dispose();
374
751
  listeners.clear();
752
+ frameHandlers.clear();
375
753
  },
376
754
  on(event, handler) {
377
755
  const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
@@ -380,20 +758,103 @@ function createGallery(scene, options) {
380
758
  return () => set.delete(handler);
381
759
  },
382
760
  emit(event, payload) {
383
- listeners.get(event)?.forEach((handler) => handler(payload));
761
+ emit(event, payload);
762
+ },
763
+ onFrame(handler) {
764
+ frameHandlers.add(handler);
765
+ return () => frameHandlers.delete(handler);
384
766
  },
385
767
  resize(w, h) {
386
768
  camera.aspect = w / h;
387
769
  camera.updateProjectionMatrix();
388
770
  renderer.setSize(w, h, false);
771
+ composer?.setSize(w, h);
772
+ ssaoPass?.setSize(w, h);
773
+ bloomPass?.setSize(w, h);
774
+ },
775
+ setRender(next) {
776
+ if (next.shadows !== void 0 && next.shadows !== render.shadows) {
777
+ render.shadows = next.shadows;
778
+ refreshShadows();
779
+ }
780
+ if (next.ssao !== void 0) render.ssao = next.ssao;
781
+ if (next.bloom !== void 0) render.bloom = next.bloom;
782
+ const aaChanged = next.antialias !== void 0 && next.antialias !== render.antialias;
783
+ if (next.antialias !== void 0) render.antialias = next.antialias;
784
+ if (next.cardboard !== void 0 && next.cardboard !== render.cardboard) {
785
+ render.cardboard = next.cardboard;
786
+ headTracking?.setEnabled(next.cardboard);
787
+ }
788
+ if (next.eyeSeparation !== void 0) {
789
+ render.eyeSeparation = next.eyeSeparation;
790
+ stereo.eyeSep = next.eyeSeparation;
791
+ }
792
+ if (ssaoPass) ssaoPass.enabled = render.ssao;
793
+ if (bloomPass) {
794
+ bloomPass.strength = render.bloom;
795
+ bloomPass.enabled = render.bloom > 0;
796
+ }
797
+ if (aaChanged && composer) buildComposer();
798
+ ensureComposer();
389
799
  }
390
800
  };
391
801
  }
392
802
  //#endregion
393
- exports.FreeRoamControls = FreeRoamControls;
803
+ //#region src/builders/texture.ts
804
+ const textureLoader = new three.TextureLoader();
805
+ /** Configure a loaded texture: tiling + color space (sRGB only for color maps). */
806
+ const configure = (tex, repeat, srgb) => {
807
+ tex.wrapS = three.RepeatWrapping;
808
+ tex.wrapT = three.RepeatWrapping;
809
+ tex.repeat.set(repeat[0], repeat[1]);
810
+ tex.colorSpace = srgb ? three.SRGBColorSpace : three.NoColorSpace;
811
+ tex.needsUpdate = true;
812
+ };
813
+ /**
814
+ * Load one map and assign it to `material[slot]` once ready. `.exr` maps go
815
+ * through EXRLoader, imported on demand so it only ships in the bundle when a
816
+ * scene actually uses an EXR map; everything else uses the image loader.
817
+ */
818
+ const loadMap = (material, slot, src, repeat, srgb) => {
819
+ const assign = (tex) => {
820
+ configure(tex, repeat, srgb);
821
+ material[slot] = tex;
822
+ material.needsUpdate = true;
823
+ };
824
+ if (src.toLowerCase().endsWith(".exr")) import("three/examples/jsm/loaders/EXRLoader.js").then(({ EXRLoader }) => {
825
+ new EXRLoader().load(src, assign);
826
+ });
827
+ else textureLoader.load(src, assign);
828
+ };
829
+ /**
830
+ * Apply a `SurfaceTexture` to a standard material. Returns whether a second UV
831
+ * set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must
832
+ * provide one.
833
+ */
834
+ const applyTexture = (material, texture) => {
835
+ const repeat = texture.repeat ?? [1, 1];
836
+ loadMap(material, "map", texture.src, repeat, true);
837
+ if (texture.normal) {
838
+ loadMap(material, "normalMap", texture.normal, repeat, false);
839
+ const s = texture.normalScale ?? 1;
840
+ material.normalScale = new three.Vector2(s, s);
841
+ }
842
+ if (texture.roughness) loadMap(material, "roughnessMap", texture.roughness, repeat, false);
843
+ if (texture.ao) loadMap(material, "aoMap", texture.ao, repeat, false);
844
+ return texture.ao !== void 0;
845
+ };
846
+ //#endregion
847
+ exports.COLLIDERS = COLLIDERS;
848
+ exports.CapabilityRegistry = CapabilityRegistry;
849
+ exports.EDGES = EDGES;
850
+ exports.EYE_HEIGHT = EYE_HEIGHT;
851
+ exports.InputBus = InputBus;
852
+ exports.InteractionController = InteractionController;
853
+ exports.ROAM_CONTROL = ROAM_CONTROL;
394
854
  exports.SceneManager = SceneManager;
395
- exports.buildArtwork = buildArtwork;
396
- exports.buildWall = buildWall;
855
+ exports.TERRAIN = TERRAIN;
856
+ exports.applyTexture = applyTexture;
397
857
  exports.createGallery = createGallery;
858
+ exports.isSystemPlugin = isSystemPlugin;
398
859
 
399
860
  //# sourceMappingURL=index.cjs.map