@snailicid3/gbt-scope 0.0.1

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 ADDED
@@ -0,0 +1,702 @@
1
+ /*
2
+ * @snailicid3/gbt-scope v0.0.1
3
+ * Module: GbtScope
4
+ * (c) 2026 Gillian Tunney
5
+ * React components and hooks for the operator user interface.
6
+ * https://github.com/gbtunney/gbt-monorepov2
7
+ * Released under the MIT License.
8
+ * Build: 9/9/2026, 7:05:01 AM
9
+ */
10
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
11
+ //#region \0rolldown/runtime.js
12
+ var __create = Object.create;
13
+ var __defProp = Object.defineProperty;
14
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
15
+ var __getOwnPropNames = Object.getOwnPropertyNames;
16
+ var __getProtoOf = Object.getPrototypeOf;
17
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
20
+ key = keys[i];
21
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
22
+ get: ((k) => from[k]).bind(null, key),
23
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
24
+ });
25
+ }
26
+ return to;
27
+ };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
29
+ value: mod,
30
+ enumerable: true
31
+ }) : target, mod));
32
+ //#endregion
33
+ let _babylonjs_core = require("@babylonjs/core");
34
+ let _snailicid3_color = require("@snailicid3/color");
35
+ let babylonjs_hook = require("babylonjs-hook");
36
+ babylonjs_hook = __toESM(babylonjs_hook, 1);
37
+ let react = require("react");
38
+ let react_jsx_runtime = require("react/jsx-runtime");
39
+ //#region src/helpers.ts
40
+ const getResolution = (dimensions) => {
41
+ const { height: _height, width: _width } = dimensions;
42
+ const aA = _height / _width > 1 ? _width / _height : 1;
43
+ const aB = _height / _width > 1 ? 1 : _height / _width;
44
+ return new _babylonjs_core.Vector4(_width, _height, aA, aB);
45
+ };
46
+ const { x, y, z } = _babylonjs_core.Vector3.Zero();
47
+ const setOrthoCamera = (scene, camera, { enabled = true, ortho = true, target = [
48
+ 0,
49
+ 0,
50
+ 0
51
+ ] }) => {
52
+ camera.setTarget(new _babylonjs_core.Vector3(...target));
53
+ camera.mode = _babylonjs_core.Camera.ORTHOGRAPHIC_CAMERA;
54
+ if (enabled) camera.attachControl(scene.getEngine().getRenderingCanvas(), true);
55
+ else camera.detachControl();
56
+ return camera;
57
+ };
58
+ const setRotateCameraPosition = (camera, scene, { enabled = true, hRotation = 0, mouseWheelSpeed = .01, position = [
59
+ x,
60
+ y,
61
+ z
62
+ ], radius = 10, target = [
63
+ 0,
64
+ 0,
65
+ 0
66
+ ], vRotation = 0 }) => {
67
+ camera.alpha = hRotation;
68
+ camera.beta = vRotation;
69
+ camera.radius = radius;
70
+ camera.wheelDeltaPercentage = mouseWheelSpeed;
71
+ camera.setTarget(new _babylonjs_core.Vector3(...target));
72
+ if (enabled) camera.attachControl(scene.getEngine().getRenderingCanvas(), true);
73
+ else camera.detachControl();
74
+ };
75
+ //#endregion
76
+ //#region src/materials/shader-radial-symmetry.ts
77
+ const vertexShader = `
78
+ precision highp float;
79
+
80
+ // Attributes
81
+ attribute vec3 position;
82
+ attribute vec2 uv;
83
+
84
+ // Uniforms
85
+ uniform mat4 worldViewProjection;
86
+
87
+ // Varying
88
+ varying vec2 vUv;
89
+
90
+ void main() {
91
+ vUv = uv;
92
+ gl_Position = worldViewProjection * vec4(position, 1.0);
93
+ }
94
+ `;
95
+ const fragmentShader = `
96
+ precision mediump float;
97
+
98
+ // Uniforms
99
+ uniform sampler2D uTexture;
100
+ uniform vec4 resolution;
101
+ uniform float uOpacity;
102
+ uniform float segments;
103
+ uniform vec2 uOffset;
104
+ uniform float uRotation;
105
+ uniform float uOffsetAmount;
106
+ uniform float uRotationAmount;
107
+ uniform float uScaleFactor;
108
+ uniform float uImageAspect;
109
+ uniform float uTiling; // Tiling factor for the entire pattern
110
+ uniform float uTileMode; // 0 = none, 1 = repeat, 2 = mirror
111
+
112
+ // Varying
113
+ varying vec2 vUv;
114
+
115
+ const float PI = 3.14159265359;
116
+
117
+ vec2 adjustUV(vec2 uv, vec2 offset, float rotation) {
118
+ float cosRot = cos(rotation * uRotationAmount);
119
+ float sinRot = sin(rotation * uRotationAmount);
120
+ mat2 rotMat = mat2(cosRot, -sinRot, sinRot, cosRot);
121
+ vec2 rotatedUV = rotMat * (uv - vec2(0.5)) + vec2(0.5); // Apply rotation first
122
+ return rotatedUV + offset * uOffsetAmount; // Apply offset after rotation
123
+ }
124
+
125
+ // Mirrored repeat: even tiles use fract, odd tiles use the reversed fract,
126
+ // producing seamless mirrored edges instead of hard wraps.
127
+ vec2 mirrorUV(vec2 uv) {
128
+ vec2 tile = floor(uv);
129
+ vec2 odd = mod(tile, 2.0);
130
+ return mix(fract(uv), 1.0 - fract(uv), odd);
131
+ }
132
+
133
+ void main() {
134
+ // Adjust UV coordinates for resolution
135
+ vec2 newUV = (vUv - vec2(0.5)) * resolution.zw + vec2(0.5);
136
+ vec2 uv = newUV * 2.0 - 1.0;
137
+
138
+ // Convert to polar coordinates
139
+ float angle = atan(uv.y, uv.x);
140
+ float radius = length(uv);
141
+
142
+ // Apply kaleidoscope effect
143
+ float segment = PI * 2.0 / segments;
144
+ angle = mod(angle, segment);
145
+ angle = segment - abs(segment / 2.0 - angle);
146
+ uv = radius * vec2(cos(angle), sin(angle));
147
+
148
+ // Scale the pattern
149
+ float scale = 1.0 / uScaleFactor;
150
+
151
+ // Apply tiling to the entire pattern based on the selected tile mode
152
+ vec2 scaledUV = uv * uTiling;
153
+ vec2 tiledUV;
154
+ if (uTileMode < 0.5) {
155
+ tiledUV = uv; // none: no wrapping
156
+ } else if (uTileMode < 1.5) {
157
+ tiledUV = fract(scaledUV); // repeat: square repeats (historical behavior)
158
+ } else {
159
+ tiledUV = mirrorUV(scaledUV); // mirror: seamless mirrored repeats
160
+ }
161
+
162
+ // Adjust UV for texture sampling
163
+ vec2 adjustedUV = adjustUV(tiledUV * scale + scale, uOffset, uRotation);
164
+ vec2 aspectCorrectedUV = vec2(adjustedUV.x, adjustedUV.y * uImageAspect);
165
+
166
+ // Sample the texture
167
+ vec4 color = texture2D(uTexture, aspectCorrectedUV);
168
+ color.a *= uOpacity;
169
+
170
+ // Output the final color
171
+ gl_FragColor = color;
172
+ }
173
+ `;
174
+ //#endregion
175
+ //#region src/motion/curve.ts
176
+ /**
177
+ * Maps an input value through a {@link GbtScopeCurve}: deadzone → gain → exponent → clamp → optional invert. The input
178
+ * is treated by magnitude (`Math.abs`), so direction is supplied by `invert`, not the sign of `value`.
179
+ */
180
+ const applyCurve = (value, curve = {}) => {
181
+ const { deadzone = 0, exponent = 1, invert = false, max = 1, min = 0, multiplier = 1 } = curve;
182
+ const normalized = Math.max(0, Math.abs(value) - deadzone);
183
+ const curved = Math.pow(normalized * multiplier, exponent);
184
+ const clamped = Math.min(Math.max(curved, min), max);
185
+ return invert ? -clamped : clamped;
186
+ };
187
+ /**
188
+ * Type guard distinguishing the legacy `[min, max]` tuple form of `mouse_curve` from the richer {@link GbtScopeCurve}
189
+ * object form.
190
+ */
191
+ const isTupleCurve = (value) => {
192
+ if (!Array.isArray(value)) return false;
193
+ const items = value;
194
+ return items.length === 2 && typeof items[0] === "number" && typeof items[1] === "number";
195
+ };
196
+ /**
197
+ * Bridges the legacy `[min, max]` tuple (plus a separate `mouse_multiplier`) into an equivalent {@link GbtScopeCurve}.
198
+ * With the historical defaults `[0, 0.015]` and multiplier `0.01`, `applyCurve` reproduces the old inline
199
+ * `Math.min(Math.max(dist * mult, min), max)` clamp exactly.
200
+ */
201
+ const tupleToGbtScopeCurve = (tuple, multiplier = 1) => ({
202
+ max: tuple[1],
203
+ min: tuple[0],
204
+ multiplier
205
+ });
206
+ //#endregion
207
+ //#region src/motion/animator.ts
208
+ /**
209
+ * Applies every animator to a copy of `state` for one frame and returns the new state. Each animator's source value is
210
+ * curved, scaled by `speed * delta` (frame-rate independent), then added to or set on its target.
211
+ */
212
+ const applyAnimators = (state, animators, inputs) => {
213
+ const next = {
214
+ ...state,
215
+ offset: [...state.offset]
216
+ };
217
+ const getSourceValue = (source) => {
218
+ switch (source) {
219
+ case "mouseDistance": return inputs.mouseDistance;
220
+ case "scrollProgress": return inputs.scrollProgress;
221
+ case "scrollVelocity": return inputs.scrollVelocity;
222
+ case "time": return inputs.time;
223
+ }
224
+ };
225
+ animators.forEach((anim) => {
226
+ const raw = getSourceValue(anim.source);
227
+ const curved = applyCurve(raw, anim.curve);
228
+ const value = (anim.speed ?? 1) * curved * inputs.delta;
229
+ const apply = (current) => anim.mode === "set" ? value : current + value;
230
+ switch (anim.target) {
231
+ case "offset.x":
232
+ next.offset = [apply(next.offset[0]), next.offset[1]];
233
+ break;
234
+ case "offset.y":
235
+ next.offset = [next.offset[0], apply(next.offset[1])];
236
+ break;
237
+ case "opacity":
238
+ next.opacity = apply(next.opacity);
239
+ break;
240
+ case "rotation":
241
+ next.rotation = apply(next.rotation);
242
+ break;
243
+ case "scaleFactor": next.scaleFactor = apply(next.scaleFactor);
244
+ }
245
+ });
246
+ return next;
247
+ };
248
+ //#endregion
249
+ //#region src/motion/driver.ts
250
+ /** Pushes a {@link GbtScopeState} onto the shader material's uniforms. */
251
+ const writeState = (material, state) => {
252
+ material.setFloat("uRotation", state.rotation);
253
+ material.setFloat("uScaleFactor", state.scaleFactor);
254
+ material.setFloat("uOpacity", state.opacity);
255
+ material.setVector2("uOffset", new _babylonjs_core.Vector2(state.offset[0], state.offset[1]));
256
+ };
257
+ /**
258
+ * Registers a single render-loop observer that drives the material's animated uniforms from the animators + live
259
+ * inputs, frame-rate independent via `engine.getDeltaTime()`. Replaces Babylon's Animation API. Returns a dispose
260
+ * function that removes the observer.
261
+ */
262
+ const createGbtScopeDriver = (scene, material, { animatorsRef, overridesRef, pointer, scroll, stateRef }) => {
263
+ let time = 0;
264
+ const observer = scene.onBeforeRenderObservable.add(() => {
265
+ const delta = scene.getEngine().getDeltaTime() / 1e3;
266
+ time += delta;
267
+ const overrides = overridesRef?.current;
268
+ const inputs = {
269
+ delta,
270
+ mouseDistance: overrides?.mouseDistance ?? Math.hypot(pointer.state.x, pointer.state.y),
271
+ scrollProgress: overrides?.scrollProgress ?? scroll.state.progress,
272
+ scrollVelocity: overrides?.scrollVelocity ?? scroll.state.velocity,
273
+ time
274
+ };
275
+ stateRef.current = applyAnimators(stateRef.current, animatorsRef.current, inputs);
276
+ writeState(material, stateRef.current);
277
+ scroll.decay();
278
+ });
279
+ return () => {
280
+ scene.onBeforeRenderObservable.remove(observer);
281
+ };
282
+ };
283
+ //#endregion
284
+ //#region src/types.ts
285
+ /**
286
+ * Canonical default values for {@link GbtScopeMaterialProps}. `src` is required and has no default. Imported by
287
+ * component defaults and Storybook args so the defaults live in a single place.
288
+ */
289
+ const defaultGbtScopeMaterialProps = {
290
+ imageAspect: 1,
291
+ offset: [0, 0],
292
+ offsetScale: 1,
293
+ opacity: 1,
294
+ rotation: 0,
295
+ rotationScale: 1,
296
+ scaleFactor: 1,
297
+ segments: 6,
298
+ tileMode: "repeat",
299
+ tiling: 1
300
+ };
301
+ //#endregion
302
+ //#region src/components/GbtScopeMaterial.tsx
303
+ const DEFAULT_DIMENSIONS = {
304
+ height: 1200,
305
+ width: 1200
306
+ };
307
+ /** Maps a {@link GbtScopeTileMode} to the shader's `uTileMode` float. */
308
+ const tileModeToFloat = (mode) => mode === "none" ? 0 : mode === "mirror" ? 2 : 1;
309
+ const UNIFORMS = [
310
+ "worldViewProjection",
311
+ "uTexture",
312
+ "resolution",
313
+ "uOpacity",
314
+ "segments",
315
+ "uOffset",
316
+ "uRotation",
317
+ "uOffsetAmount",
318
+ "uRotationAmount",
319
+ "uScaleFactor",
320
+ "uImageAspect",
321
+ "uTiling",
322
+ "uTileMode"
323
+ ];
324
+ /**
325
+ * Kaleidoscope shader material applied to a Babylon mesh. Static uniforms update reactively from props; the animated
326
+ * uniforms (rotation/offset/scaleFactor/ opacity) are driven each frame by {@link createGbtScopeDriver} from the
327
+ * `animators` + live pointer/scroll inputs. No Babylon Animation is used.
328
+ */
329
+ const GbtScopeMaterial = ({ animators = [], dimensions = DEFAULT_DIMENSIONS, imageAspect = defaultGbtScopeMaterialProps.imageAspect, inputOverrides, mesh, name = "kaleidoscope", offset = defaultGbtScopeMaterialProps.offset, offsetScale = defaultGbtScopeMaterialProps.offsetScale, onInit, onUpdate, opacity = defaultGbtScopeMaterialProps.opacity, pointer, rotation = defaultGbtScopeMaterialProps.rotation, rotationScale = defaultGbtScopeMaterialProps.rotationScale, scaleFactor = defaultGbtScopeMaterialProps.scaleFactor, scroll, segments = defaultGbtScopeMaterialProps.segments, src, tileMode = defaultGbtScopeMaterialProps.tileMode, tiling = defaultGbtScopeMaterialProps.tiling }) => {
330
+ const materialRef = (0, react.useRef)(null);
331
+ const animatorsRef = (0, react.useRef)(animators);
332
+ const overridesRef = (0, react.useRef)(inputOverrides);
333
+ const stateRef = (0, react.useRef)({
334
+ offset: [offset[0], offset[1]],
335
+ opacity,
336
+ rotation,
337
+ scaleFactor
338
+ });
339
+ (0, react.useEffect)(() => {
340
+ if (!src || !mesh) return void 0;
341
+ const scene = mesh.getScene();
342
+ const material = new _babylonjs_core.ShaderMaterial(name, scene, {
343
+ fragmentSource: fragmentShader,
344
+ vertexSource: vertexShader
345
+ }, {
346
+ attributes: ["position", "uv"],
347
+ uniforms: UNIFORMS
348
+ });
349
+ material.setTexture("uTexture", new _babylonjs_core.Texture(src, scene, true, false));
350
+ mesh.material = material;
351
+ materialRef.current = material;
352
+ const disposeDriver = createGbtScopeDriver(scene, material, {
353
+ animatorsRef,
354
+ overridesRef,
355
+ pointer,
356
+ scroll,
357
+ stateRef
358
+ });
359
+ onInit?.(material);
360
+ return () => {
361
+ disposeDriver();
362
+ material.dispose();
363
+ materialRef.current = null;
364
+ };
365
+ }, [
366
+ mesh,
367
+ src,
368
+ name
369
+ ]);
370
+ (0, react.useEffect)(() => {
371
+ animatorsRef.current = animators;
372
+ }, [animators]);
373
+ (0, react.useEffect)(() => {
374
+ overridesRef.current = inputOverrides;
375
+ }, [inputOverrides]);
376
+ (0, react.useEffect)(() => {
377
+ stateRef.current = {
378
+ offset: [offset[0], offset[1]],
379
+ opacity,
380
+ rotation,
381
+ scaleFactor
382
+ };
383
+ }, [
384
+ rotation,
385
+ offset,
386
+ scaleFactor,
387
+ opacity
388
+ ]);
389
+ (0, react.useEffect)(() => {
390
+ const material = materialRef.current;
391
+ if (!material) return;
392
+ material.setVector4("resolution", getResolution(dimensions));
393
+ material.setFloat("segments", segments);
394
+ material.setFloat("uOffsetAmount", offsetScale);
395
+ material.setFloat("uRotationAmount", rotationScale);
396
+ material.setFloat("uImageAspect", imageAspect);
397
+ material.setFloat("uTiling", tiling || 1);
398
+ material.setFloat("uTileMode", tileModeToFloat(tileMode));
399
+ onUpdate?.(material);
400
+ }, [
401
+ dimensions,
402
+ segments,
403
+ offsetScale,
404
+ rotationScale,
405
+ imageAspect,
406
+ tiling,
407
+ tileMode,
408
+ onUpdate
409
+ ]);
410
+ return null;
411
+ };
412
+ //#endregion
413
+ //#region src/motion/pointer.ts
414
+ /**
415
+ * Creates a mutable pointer-state object for use inside a Babylon.js scene setup callback. Intentionally NOT a React
416
+ * hook: `onSceneReady` runs outside React's render cycle, so a hook's state would be stale inside the render
417
+ * observable. The returned `state` object is mutated in place and is safe to read every frame.
418
+ *
419
+ * Position is normalized to [-1, 1] on both axes (canvas-relative). Uses pointer events so mouse and touch behave
420
+ * uniformly: a mouse resets to [0, 0] on leaving the canvas, while a touch latches at the last tap/drag position (tap
421
+ * center to zero it) — hover-less devices would otherwise never produce input.
422
+ */
423
+ const createPointerState = () => {
424
+ const _state = {
425
+ x: 0,
426
+ y: 0
427
+ };
428
+ const handlePointerMove = (event) => {
429
+ const rect = event.currentTarget.getBoundingClientRect();
430
+ _state.x = (event.clientX - rect.left) / rect.width * 2 - 1;
431
+ _state.y = (event.clientY - rect.top) / rect.height * 2 - 1;
432
+ };
433
+ const handlePointerLeave = (event) => {
434
+ if (event.pointerType !== "mouse") return;
435
+ _state.x = 0;
436
+ _state.y = 0;
437
+ };
438
+ return {
439
+ attach: (canvas) => {
440
+ canvas.addEventListener("pointerdown", handlePointerMove);
441
+ canvas.addEventListener("pointermove", handlePointerMove);
442
+ canvas.addEventListener("pointerleave", handlePointerLeave);
443
+ },
444
+ detach: (canvas) => {
445
+ canvas.removeEventListener("pointerdown", handlePointerMove);
446
+ canvas.removeEventListener("pointermove", handlePointerMove);
447
+ canvas.removeEventListener("pointerleave", handlePointerLeave);
448
+ },
449
+ state: _state
450
+ };
451
+ };
452
+ //#endregion
453
+ //#region src/motion/scroll.ts
454
+ const readProgress = () => {
455
+ if (typeof window === "undefined" || typeof document === "undefined") return 0;
456
+ const doc = document.documentElement;
457
+ const max = doc.scrollHeight - doc.clientHeight;
458
+ return max > 0 ? Math.min(Math.max(window.scrollY / max, 0), 1) : 0;
459
+ };
460
+ /**
461
+ * Creates a mutable scroll-state object for use inside a Babylon.js scene setup callback. Tracks page scroll `progress`
462
+ * [0,1] and a wheel-driven `velocity` that the driver decays each frame. Plain factory (not a React hook) — mirrors
463
+ * {@link ./pointer.createPointerState} so it can be read from the render observable.
464
+ */
465
+ const createScrollState = () => {
466
+ const _state = {
467
+ progress: readProgress(),
468
+ velocity: 0
469
+ };
470
+ const handleWheel = (event) => {
471
+ _state.velocity += event.deltaY / 1e3;
472
+ };
473
+ const handleScroll = () => {
474
+ _state.progress = readProgress();
475
+ };
476
+ const resolve = (target) => target ?? (typeof window !== "undefined" ? window : {});
477
+ return {
478
+ attach: (target) => {
479
+ const t = resolve(target);
480
+ t.addEventListener("wheel", handleWheel, { passive: true });
481
+ t.addEventListener("scroll", handleScroll, { passive: true });
482
+ },
483
+ decay: (factor = .9) => {
484
+ _state.velocity *= factor;
485
+ if (Math.abs(_state.velocity) < 1e-4) _state.velocity = 0;
486
+ },
487
+ detach: (target) => {
488
+ const t = resolve(target);
489
+ t.removeEventListener("wheel", handleWheel);
490
+ t.removeEventListener("scroll", handleScroll);
491
+ },
492
+ state: _state
493
+ };
494
+ };
495
+ //#endregion
496
+ //#region src/components/GbtScopeFlatViewer.tsx
497
+ /** Default props for the flat viewer — single source of truth for Storybook args. */
498
+ const defaultGbtScopeFlatViewerProps = {
499
+ ...defaultGbtScopeMaterialProps,
500
+ animators: [],
501
+ aspect_ratio: 1,
502
+ bg_color: "black",
503
+ cameraSettings: {
504
+ enabled: false,
505
+ ortho: true,
506
+ target: [
507
+ 0,
508
+ 0,
509
+ 0
510
+ ]
511
+ },
512
+ name: "gbt-scope-flat",
513
+ resolution: "screen",
514
+ src: "uv-checker.png"
515
+ };
516
+ const GbtScopeFlatViewer = ({ animators = [], aspect_ratio = 1, bg_color = "black", cameraSettings = {
517
+ enabled: false,
518
+ ortho: true,
519
+ target: [
520
+ 0,
521
+ 0,
522
+ 0
523
+ ]
524
+ }, imageAspect = defaultGbtScopeMaterialProps.imageAspect, inputOverrides, name = "gbt-scope-flat", offset = defaultGbtScopeMaterialProps.offset, offsetScale = defaultGbtScopeMaterialProps.offsetScale, opacity = defaultGbtScopeMaterialProps.opacity, resolution = "screen", rotation = defaultGbtScopeMaterialProps.rotation, rotationScale = defaultGbtScopeMaterialProps.rotationScale, scaleFactor = defaultGbtScopeMaterialProps.scaleFactor, segments = defaultGbtScopeMaterialProps.segments, src, tileMode = defaultGbtScopeMaterialProps.tileMode, tiling = defaultGbtScopeMaterialProps.tiling }) => {
525
+ const [scene, setScene] = (0, react.useState)(null);
526
+ const [plane, setPlane] = (0, react.useState)(null);
527
+ const [pointerState] = (0, react.useState)(createPointerState);
528
+ const [scrollState] = (0, react.useState)(createScrollState);
529
+ const customStyle = {
530
+ backgroundColor: (0, _snailicid3_color.isValidColor)(bg_color) ? (0, _snailicid3_color.parseColorToHexStrict)(bg_color) : "initial",
531
+ border: "2px solid green",
532
+ margin: 0,
533
+ padding: 0,
534
+ position: "relative",
535
+ ...aspect_ratio !== "parent" ? { aspectRatio: aspect_ratio } : {}
536
+ };
537
+ const dimensions = resolution === "screen" ? scene !== null ? {
538
+ height: scene.getEngine().getRenderHeight(),
539
+ width: scene.getEngine().getRenderWidth()
540
+ } : void 0 : resolution ?? void 0;
541
+ const onSceneReady = (_scene) => {
542
+ _scene.clearColor = new _babylonjs_core.Color4(0, 0, 0, 1);
543
+ setScene(_scene);
544
+ const camera = new _babylonjs_core.FreeCamera(`camera_${name}`, new _babylonjs_core.Vector3(0, 0, -10), _scene);
545
+ setOrthoCamera(_scene, camera, cameraSettings);
546
+ new _babylonjs_core.HemisphericLight(`light_${name}`, new _babylonjs_core.Vector3(0, 1, 0), _scene);
547
+ const planeMesh = _babylonjs_core.MeshBuilder.CreatePlane(`plane_${name}`, {
548
+ height: _scene.getEngine().getRenderHeight(),
549
+ width: _scene.getEngine().getRenderWidth()
550
+ }, _scene);
551
+ setPlane(planeMesh);
552
+ const canvas = _scene.getEngine().getRenderingCanvas();
553
+ if (canvas) {
554
+ canvas.tabIndex = 1;
555
+ canvas.addEventListener("keydown", (event) => {
556
+ if (event.key === "Escape") setOrthoCamera(_scene, camera, cameraSettings);
557
+ });
558
+ pointerState.attach(canvas);
559
+ scrollState.attach();
560
+ _scene.onDisposeObservable.add(() => {
561
+ pointerState.detach(canvas);
562
+ scrollState.detach();
563
+ });
564
+ }
565
+ };
566
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
567
+ style: customStyle,
568
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(babylonjs_hook.default, {
569
+ antialias: true,
570
+ id: "my-canvas",
571
+ onSceneReady,
572
+ style: {
573
+ height: "100%",
574
+ width: "100%"
575
+ },
576
+ children: scene && plane && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GbtScopeMaterial, {
577
+ animators,
578
+ dimensions,
579
+ imageAspect,
580
+ inputOverrides,
581
+ mesh: plane,
582
+ name: `material_${name}`,
583
+ offset,
584
+ offsetScale,
585
+ opacity,
586
+ pointer: pointerState,
587
+ rotation,
588
+ rotationScale,
589
+ scaleFactor,
590
+ scroll: scrollState,
591
+ segments,
592
+ src,
593
+ tileMode,
594
+ tiling
595
+ })
596
+ })
597
+ });
598
+ };
599
+ //#endregion
600
+ //#region src/components/GbtScopeMeshViewer.tsx
601
+ /** Default props for the 3D mesh viewer — single source of truth for Storybook args. */
602
+ const defaultGbtScopeMeshViewerProps = {
603
+ ...defaultGbtScopeMaterialProps,
604
+ animators: [],
605
+ aspect_ratio: 1,
606
+ bg_color: "black",
607
+ cameraSettings: {
608
+ enabled: true,
609
+ hRotation: Math.PI / 2,
610
+ vRotation: Math.PI / 4
611
+ },
612
+ name: "gbt-scope-mesh",
613
+ resolution: null,
614
+ src: "uv-checker.png"
615
+ };
616
+ const GbtScopeMeshViewer = ({ animators = [], aspect_ratio = 1, bg_color = "black", cameraSettings = {
617
+ enabled: true,
618
+ hRotation: Math.PI / 2,
619
+ vRotation: Math.PI / 4
620
+ }, imageAspect = defaultGbtScopeMaterialProps.imageAspect, inputOverrides, name = "gbt-scope-mesh", offset = defaultGbtScopeMaterialProps.offset, offsetScale = defaultGbtScopeMaterialProps.offsetScale, opacity = defaultGbtScopeMaterialProps.opacity, resolution = null, rotation = defaultGbtScopeMaterialProps.rotation, rotationScale = defaultGbtScopeMaterialProps.rotationScale, scaleFactor = defaultGbtScopeMaterialProps.scaleFactor, segments = defaultGbtScopeMaterialProps.segments, src, tileMode = defaultGbtScopeMaterialProps.tileMode, tiling = defaultGbtScopeMaterialProps.tiling }) => {
621
+ const [scene, setScene] = (0, react.useState)(null);
622
+ const [box, setBox] = (0, react.useState)(null);
623
+ const [pointerState] = (0, react.useState)(createPointerState);
624
+ const [scrollState] = (0, react.useState)(createScrollState);
625
+ const customStyle = {
626
+ backgroundColor: (0, _snailicid3_color.isValidColor)(bg_color) ? (0, _snailicid3_color.parseColorToHexStrict)(bg_color) : "initial",
627
+ border: "2px solid green",
628
+ ...aspect_ratio !== "parent" ? { aspectRatio: aspect_ratio } : {}
629
+ };
630
+ const dimensions = resolution === "screen" ? scene !== null ? {
631
+ height: scene.getEngine().getRenderHeight(),
632
+ width: scene.getEngine().getRenderWidth()
633
+ } : void 0 : resolution ?? void 0;
634
+ const onSceneReady = (_scene) => {
635
+ _scene.clearColor = new _babylonjs_core.Color4(0, 0, 0, 0);
636
+ setScene(_scene);
637
+ const camera = new _babylonjs_core.ArcRotateCamera(`camera_${name}`, cameraSettings.hRotation ?? Math.PI / 2, cameraSettings.vRotation ?? Math.PI / 4, 10, _babylonjs_core.Vector3.Zero(), _scene);
638
+ setRotateCameraPosition(camera, _scene, cameraSettings);
639
+ new _babylonjs_core.HemisphericLight(`light_${name}`, new _babylonjs_core.Vector3(0, 1, 0), _scene);
640
+ const boxMesh = _babylonjs_core.MeshBuilder.CreateBox(`box_${name}`, { size: 2 }, _scene);
641
+ boxMesh.position.y = 1;
642
+ setBox(boxMesh);
643
+ const canvas = _scene.getEngine().getRenderingCanvas();
644
+ if (canvas) {
645
+ canvas.tabIndex = 1;
646
+ canvas.addEventListener("keydown", (event) => {
647
+ if (event.key === "Escape") setRotateCameraPosition(camera, _scene, cameraSettings);
648
+ });
649
+ pointerState.attach(canvas);
650
+ scrollState.attach();
651
+ _scene.onDisposeObservable.add(() => {
652
+ pointerState.detach(canvas);
653
+ scrollState.detach();
654
+ });
655
+ }
656
+ };
657
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
658
+ style: customStyle,
659
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(babylonjs_hook.default, {
660
+ antialias: true,
661
+ id: "my-canvas",
662
+ onSceneReady,
663
+ style: {
664
+ height: "100%",
665
+ width: "100%"
666
+ },
667
+ children: scene && box && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GbtScopeMaterial, {
668
+ animators,
669
+ dimensions,
670
+ imageAspect,
671
+ inputOverrides,
672
+ mesh: box,
673
+ name: `material_${name}`,
674
+ offset,
675
+ offsetScale,
676
+ opacity,
677
+ pointer: pointerState,
678
+ rotation,
679
+ rotationScale,
680
+ scaleFactor,
681
+ scroll: scrollState,
682
+ segments,
683
+ src,
684
+ tileMode,
685
+ tiling
686
+ })
687
+ })
688
+ });
689
+ };
690
+ //#endregion
691
+ exports.GbtScopeFlatViewer = GbtScopeFlatViewer;
692
+ exports.GbtScopeMaterial = GbtScopeMaterial;
693
+ exports.GbtScopeMeshViewer = GbtScopeMeshViewer;
694
+ exports.applyAnimators = applyAnimators;
695
+ exports.applyCurve = applyCurve;
696
+ exports.createPointerState = createPointerState;
697
+ exports.createScrollState = createScrollState;
698
+ exports.defaultGbtScopeFlatViewerProps = defaultGbtScopeFlatViewerProps;
699
+ exports.defaultGbtScopeMaterialProps = defaultGbtScopeMaterialProps;
700
+ exports.defaultGbtScopeMeshViewerProps = defaultGbtScopeMeshViewerProps;
701
+ exports.isTupleCurve = isTupleCurve;
702
+ exports.tupleToGbtScopeCurve = tupleToGbtScopeCurve;