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