@fonsecabarreto/genesis-gl-react 0.1.3 → 0.1.31
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/chunk-7BVTZKWZ.js +394 -0
- package/dist/chunk-7BVTZKWZ.js.map +1 -0
- package/dist/chunk-GDDOUPFO.js +66 -0
- package/dist/chunk-GDDOUPFO.js.map +1 -0
- package/dist/chunk-MINHVZ2M.js +267 -0
- package/dist/chunk-MINHVZ2M.js.map +1 -0
- package/dist/chunk-PJM4LJXC.js +394 -0
- package/dist/chunk-PJM4LJXC.js.map +1 -0
- package/dist/chunk-QXHXGH2T.js +4733 -0
- package/dist/chunk-QXHXGH2T.js.map +1 -0
- package/dist/chunk-UVQX6HJJ.js +394 -0
- package/dist/chunk-UVQX6HJJ.js.map +1 -0
- package/dist/chunk-VAMVRTC4.js +66 -0
- package/dist/chunk-VAMVRTC4.js.map +1 -0
- package/dist/chunk-XJKE4L2D.js +267 -0
- package/dist/chunk-XJKE4L2D.js.map +1 -0
- package/dist/components/index.d.ts +60 -0
- package/dist/components/index.js +2 -2
- package/dist/hooks/index.d.ts +64 -0
- package/dist/hooks/index.js +4 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/useGenesisGL-VdB4J3Hl.d.ts +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/hooks/useGenesisGL.ts
|
|
2
|
+
import { useRef, useEffect, useState } from "react";
|
|
3
|
+
import { WebGLCore as WebGLCoreImpl } from "@fonsecabarreto/genesis-gl-core/Core/classes/WebGLCore";
|
|
4
|
+
import { Viewport as ViewportImpl } from "@fonsecabarreto/genesis-gl-core/Core/classes/Viewport";
|
|
5
|
+
import { Renderer as RendererImpl } from "@fonsecabarreto/genesis-gl-core/Core/classes/Renderer";
|
|
6
|
+
import { Scene as SceneImpl } from "@fonsecabarreto/genesis-gl-core/Core/classes/Scene";
|
|
7
|
+
import { loadOBJWithMTL } from "@fonsecabarreto/genesis-gl-core/Core/utils/parse-obj";
|
|
8
|
+
var nullLoadOBJ = () => Promise.reject(new Error("GenesisGL not initialized"));
|
|
9
|
+
function useGenesisGL(options) {
|
|
10
|
+
const [context, setContext] = useState({
|
|
11
|
+
renderer: null,
|
|
12
|
+
scene: null,
|
|
13
|
+
viewport: null,
|
|
14
|
+
webglCore: null,
|
|
15
|
+
loadOBJ: nullLoadOBJ
|
|
16
|
+
});
|
|
17
|
+
const contextRef = useRef(context);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const {
|
|
20
|
+
canvasRef,
|
|
21
|
+
onReady,
|
|
22
|
+
onError,
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
initialYaw,
|
|
26
|
+
initialPitch,
|
|
27
|
+
initialZoom
|
|
28
|
+
} = options;
|
|
29
|
+
if (!canvasRef.current) return;
|
|
30
|
+
try {
|
|
31
|
+
const canvas = canvasRef.current;
|
|
32
|
+
const vpWidth = width ?? window.innerWidth;
|
|
33
|
+
const vpHeight = height ?? window.innerHeight;
|
|
34
|
+
const webglCore = new WebGLCoreImpl(canvas);
|
|
35
|
+
const viewport = new ViewportImpl(canvas, vpWidth, vpHeight, webglCore, {
|
|
36
|
+
pointerLock: false
|
|
37
|
+
});
|
|
38
|
+
const renderer = new RendererImpl(webglCore, viewport);
|
|
39
|
+
const scene = SceneImpl.withDefaultLights(webglCore);
|
|
40
|
+
viewport.camera.target = [0, 0, 0];
|
|
41
|
+
viewport.camera.position = [4, 2, 8];
|
|
42
|
+
if (initialYaw !== void 0) viewport.camera.yaw = initialYaw;
|
|
43
|
+
if (initialPitch !== void 0) viewport.camera.pitch = initialPitch;
|
|
44
|
+
if (initialZoom !== void 0) viewport.camera.setZoom(initialZoom);
|
|
45
|
+
viewport.camera.invalidate();
|
|
46
|
+
const loadOBJ = (objUrl, opts = {}) => loadOBJWithMTL(
|
|
47
|
+
webglCore,
|
|
48
|
+
objUrl,
|
|
49
|
+
opts.mtlUrl ?? null,
|
|
50
|
+
opts.translation,
|
|
51
|
+
opts.scale
|
|
52
|
+
);
|
|
53
|
+
const ctx = {
|
|
54
|
+
renderer,
|
|
55
|
+
scene,
|
|
56
|
+
viewport,
|
|
57
|
+
webglCore,
|
|
58
|
+
loadOBJ
|
|
59
|
+
};
|
|
60
|
+
contextRef.current = ctx;
|
|
61
|
+
setContext(ctx);
|
|
62
|
+
if (onReady) onReady(ctx);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
65
|
+
console.error("Failed to initialize GenesisGL:", err);
|
|
66
|
+
if (onError) onError(err);
|
|
67
|
+
}
|
|
68
|
+
return () => {
|
|
69
|
+
const { scene, viewport, webglCore } = contextRef.current;
|
|
70
|
+
if (scene && webglCore) scene.dispose(webglCore);
|
|
71
|
+
if (viewport) viewport.dispose();
|
|
72
|
+
if (webglCore) webglCore.dispose();
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/hooks/useRenderer.ts
|
|
79
|
+
import { useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
80
|
+
function useRenderer(options) {
|
|
81
|
+
const { renderer, onFrame, enabled = true } = options;
|
|
82
|
+
const animationIdRef = useRef2(null);
|
|
83
|
+
useEffect2(() => {
|
|
84
|
+
if (!renderer || !enabled) return;
|
|
85
|
+
const animate = (time) => {
|
|
86
|
+
if (onFrame) onFrame(time);
|
|
87
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
88
|
+
};
|
|
89
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
90
|
+
return () => {
|
|
91
|
+
if (animationIdRef.current !== null) {
|
|
92
|
+
cancelAnimationFrame(animationIdRef.current);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}, [renderer, onFrame, enabled]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/hooks/useWorldToScreen.ts
|
|
99
|
+
import { useState as useState2, useCallback } from "react";
|
|
100
|
+
function mat4TransformVec4(m, x, y, z, w) {
|
|
101
|
+
return {
|
|
102
|
+
x: m[0] * x + m[4] * y + m[8] * z + m[12] * w,
|
|
103
|
+
y: m[1] * x + m[5] * y + m[9] * z + m[13] * w,
|
|
104
|
+
z: m[2] * x + m[6] * y + m[10] * z + m[14] * w,
|
|
105
|
+
w: m[3] * x + m[7] * y + m[11] * z + m[15] * w
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function useWorldToScreen(context) {
|
|
109
|
+
const [, forceUpdate] = useState2(0);
|
|
110
|
+
const project = useCallback(
|
|
111
|
+
(worldX, worldY, worldZ) => {
|
|
112
|
+
if (!context?.viewport) return { x: 0, y: 0, visible: false };
|
|
113
|
+
const { viewport } = context;
|
|
114
|
+
const camera = viewport.camera;
|
|
115
|
+
const canvas = viewport.getCanvas();
|
|
116
|
+
const view = camera.getViewMatrix();
|
|
117
|
+
const proj = camera.getProjectionMatrix();
|
|
118
|
+
const v = mat4TransformVec4(view, worldX, worldY, worldZ, 1);
|
|
119
|
+
const c = mat4TransformVec4(proj, v.x, v.y, v.z, v.w);
|
|
120
|
+
if (Math.abs(c.w) < 1e-6) return { x: 0, y: 0, visible: false };
|
|
121
|
+
const ndcX = c.x / c.w;
|
|
122
|
+
const ndcY = c.y / c.w;
|
|
123
|
+
const ndcZ = c.z / c.w;
|
|
124
|
+
const visible = ndcZ > -1 && ndcZ < 1 && Math.abs(ndcX) < 1.2 && Math.abs(ndcY) < 1.2;
|
|
125
|
+
return {
|
|
126
|
+
x: (ndcX + 1) * 0.5 * canvas.clientWidth,
|
|
127
|
+
y: (1 - ndcY) * 0.5 * canvas.clientHeight,
|
|
128
|
+
visible
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
[context]
|
|
132
|
+
);
|
|
133
|
+
const tick = useCallback(() => forceUpdate((n) => n + 1), []);
|
|
134
|
+
return { project, tick };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/hooks/useCameraControls.ts
|
|
138
|
+
import { useCallback as useCallback2, useState as useState3 } from "react";
|
|
139
|
+
var DEFAULT_YAW = 0;
|
|
140
|
+
var DEFAULT_PITCH = 0;
|
|
141
|
+
var DEFAULT_ZOOM = 1;
|
|
142
|
+
function useCameraControls(ctx) {
|
|
143
|
+
const [camera, setCameraState] = useState3({
|
|
144
|
+
yaw: DEFAULT_YAW,
|
|
145
|
+
pitch: DEFAULT_PITCH,
|
|
146
|
+
zoom: DEFAULT_ZOOM
|
|
147
|
+
});
|
|
148
|
+
const sync = useCallback2((c) => {
|
|
149
|
+
setCameraState({ yaw: c.yaw, pitch: c.pitch, zoom: c.zoom });
|
|
150
|
+
}, []);
|
|
151
|
+
const rotate = useCallback2(
|
|
152
|
+
(deltaYaw, deltaPitch) => {
|
|
153
|
+
const c = ctx?.viewport?.camera;
|
|
154
|
+
if (!c) return;
|
|
155
|
+
c.rotate(deltaYaw, deltaPitch);
|
|
156
|
+
sync(c);
|
|
157
|
+
},
|
|
158
|
+
[ctx, sync]
|
|
159
|
+
);
|
|
160
|
+
const zoomBy = useCallback2(
|
|
161
|
+
(delta) => {
|
|
162
|
+
const c = ctx?.viewport?.camera;
|
|
163
|
+
if (!c) return;
|
|
164
|
+
c.zoomBy(delta);
|
|
165
|
+
sync(c);
|
|
166
|
+
},
|
|
167
|
+
[ctx, sync]
|
|
168
|
+
);
|
|
169
|
+
const setZoom = useCallback2(
|
|
170
|
+
(zoom) => {
|
|
171
|
+
const c = ctx?.viewport?.camera;
|
|
172
|
+
if (!c) return;
|
|
173
|
+
c.setZoom(zoom);
|
|
174
|
+
sync(c);
|
|
175
|
+
},
|
|
176
|
+
[ctx, sync]
|
|
177
|
+
);
|
|
178
|
+
const setYaw = useCallback2(
|
|
179
|
+
(yaw) => {
|
|
180
|
+
const c = ctx?.viewport?.camera;
|
|
181
|
+
if (!c) return;
|
|
182
|
+
c.yaw = yaw;
|
|
183
|
+
c.invalidate();
|
|
184
|
+
sync(c);
|
|
185
|
+
},
|
|
186
|
+
[ctx, sync]
|
|
187
|
+
);
|
|
188
|
+
const setPitch = useCallback2(
|
|
189
|
+
(pitch) => {
|
|
190
|
+
const c = ctx?.viewport?.camera;
|
|
191
|
+
if (!c) return;
|
|
192
|
+
c.pitch = pitch;
|
|
193
|
+
c.invalidate();
|
|
194
|
+
sync(c);
|
|
195
|
+
},
|
|
196
|
+
[ctx, sync]
|
|
197
|
+
);
|
|
198
|
+
const reset = useCallback2(() => {
|
|
199
|
+
const c = ctx?.viewport?.camera;
|
|
200
|
+
if (!c) return;
|
|
201
|
+
c.yaw = DEFAULT_YAW;
|
|
202
|
+
c.pitch = DEFAULT_PITCH;
|
|
203
|
+
c.invalidate();
|
|
204
|
+
c.setZoom(DEFAULT_ZOOM);
|
|
205
|
+
sync(c);
|
|
206
|
+
}, [ctx, sync]);
|
|
207
|
+
return { camera, rotate, setYaw, setPitch, zoomBy, setZoom, reset };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/hooks/useCameraMouseDrag.ts
|
|
211
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
212
|
+
import { MouseDragControl } from "@fonsecabarreto/genesis-gl-core/Core";
|
|
213
|
+
var SENSITIVITY = 8e-3;
|
|
214
|
+
function useCameraMouseDrag(ctx, canvasRef) {
|
|
215
|
+
const { rotate } = useCameraControls(ctx);
|
|
216
|
+
const rotateRef = useRef3(rotate);
|
|
217
|
+
rotateRef.current = rotate;
|
|
218
|
+
useEffect3(() => {
|
|
219
|
+
const el = canvasRef.current;
|
|
220
|
+
if (!el || !ctx) return;
|
|
221
|
+
const drag = new MouseDragControl(el);
|
|
222
|
+
const onDrag = (dx, dy, button) => {
|
|
223
|
+
if (button !== 2) return;
|
|
224
|
+
rotateRef.current(-dx * SENSITIVITY, dy * SENSITIVITY);
|
|
225
|
+
};
|
|
226
|
+
drag.onChange(onDrag);
|
|
227
|
+
drag.enable();
|
|
228
|
+
const onMouseDown = (e) => {
|
|
229
|
+
if (e.button !== 2) return;
|
|
230
|
+
el.style.cursor = "grabbing";
|
|
231
|
+
};
|
|
232
|
+
const onMouseUp = (e) => {
|
|
233
|
+
if (e.button !== 2) return;
|
|
234
|
+
el.style.cursor = "grab";
|
|
235
|
+
};
|
|
236
|
+
const onMouseEnter = () => {
|
|
237
|
+
el.style.cursor = "grab";
|
|
238
|
+
};
|
|
239
|
+
const onMouseLeave = () => {
|
|
240
|
+
el.style.cursor = "";
|
|
241
|
+
};
|
|
242
|
+
const onContextMenu = (e) => e.preventDefault();
|
|
243
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
244
|
+
el.addEventListener("mouseenter", onMouseEnter);
|
|
245
|
+
el.addEventListener("mouseleave", onMouseLeave);
|
|
246
|
+
window.addEventListener("mouseup", onMouseUp);
|
|
247
|
+
el.addEventListener("contextmenu", onContextMenu);
|
|
248
|
+
return () => {
|
|
249
|
+
drag.disable();
|
|
250
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
251
|
+
el.removeEventListener("mouseenter", onMouseEnter);
|
|
252
|
+
el.removeEventListener("mouseleave", onMouseLeave);
|
|
253
|
+
window.removeEventListener("mouseup", onMouseUp);
|
|
254
|
+
el.removeEventListener("contextmenu", onContextMenu);
|
|
255
|
+
el.style.cursor = "";
|
|
256
|
+
};
|
|
257
|
+
}, [ctx]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export {
|
|
261
|
+
useGenesisGL,
|
|
262
|
+
useRenderer,
|
|
263
|
+
useWorldToScreen,
|
|
264
|
+
useCameraControls,
|
|
265
|
+
useCameraMouseDrag
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=chunk-MINHVZ2M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useGenesisGL.ts","../src/hooks/useRenderer.ts","../src/hooks/useWorldToScreen.ts","../src/hooks/useCameraControls.ts","../src/hooks/useCameraMouseDrag.ts"],"sourcesContent":["import { useRef, useEffect, useState } from 'react';\nimport type {\n WebGLCore,\n Scene,\n Renderer,\n Viewport,\n Model,\n} from '@fonsecabarreto/genesis-gl-core/Core';\nimport { WebGLCore as WebGLCoreImpl } from '@fonsecabarreto/genesis-gl-core/Core/classes/WebGLCore';\nimport { Viewport as ViewportImpl } from '@fonsecabarreto/genesis-gl-core/Core/classes/Viewport';\nimport { Renderer as RendererImpl } from '@fonsecabarreto/genesis-gl-core/Core/classes/Renderer';\nimport { Scene as SceneImpl } from '@fonsecabarreto/genesis-gl-core/Core/classes/Scene';\nimport { loadOBJWithMTL } from '@fonsecabarreto/genesis-gl-core/Core/utils/parse-obj';\nexport interface LoadOBJOptions {\n mtlUrl?: string | null;\n translation?: [number, number, number];\n scale?: [number, number, number];\n}\n\nexport interface GenesisGLContext {\n renderer: Renderer | null;\n scene: Scene | null;\n viewport: Viewport | null;\n webglCore: WebGLCore | null;\n loadOBJ: (objUrl: string, options?: LoadOBJOptions) => Promise<Model>;\n}\n\nexport interface UseGenesisGLOptions {\n canvasRef: React.RefObject<HTMLCanvasElement>;\n width?: number;\n height?: number;\n initialYaw?: number;\n initialPitch?: number;\n initialZoom?: number;\n onReady?: (context: GenesisGLContext) => void;\n onError?: (error: Error) => void;\n}\n\nconst nullLoadOBJ = (): Promise<never> =>\n Promise.reject(new Error('GenesisGL not initialized'));\n\nexport function useGenesisGL(options: UseGenesisGLOptions): GenesisGLContext {\n const [context, setContext] = useState<GenesisGLContext>({\n renderer: null,\n scene: null,\n viewport: null,\n webglCore: null,\n loadOBJ: nullLoadOBJ,\n });\n\n const contextRef = useRef<GenesisGLContext>(context);\n\n useEffect(() => {\n const {\n canvasRef,\n onReady,\n onError,\n width,\n height,\n initialYaw,\n initialPitch,\n initialZoom,\n } = options;\n if (!canvasRef.current) return;\n\n try {\n const canvas = canvasRef.current;\n const vpWidth = width ?? window.innerWidth;\n const vpHeight = height ?? window.innerHeight;\n\n const webglCore = new WebGLCoreImpl(canvas);\n const viewport = new ViewportImpl(canvas, vpWidth, vpHeight, webglCore, {\n pointerLock: false,\n });\n const renderer = new RendererImpl(webglCore, viewport);\n const scene = SceneImpl.withDefaultLights(webglCore);\n\n viewport.camera.target = [0, 0, 0];\n viewport.camera.position = [4, 2, 8];\n if (initialYaw !== undefined) viewport.camera.yaw = initialYaw;\n if (initialPitch !== undefined) viewport.camera.pitch = initialPitch;\n if (initialZoom !== undefined) viewport.camera.setZoom(initialZoom);\n viewport.camera.invalidate();\n\n const loadOBJ = (objUrl: string, opts: LoadOBJOptions = {}) =>\n loadOBJWithMTL(\n webglCore,\n objUrl,\n opts.mtlUrl ?? null,\n opts.translation,\n opts.scale,\n );\n\n const ctx: GenesisGLContext = {\n renderer,\n scene,\n viewport,\n webglCore,\n loadOBJ,\n };\n contextRef.current = ctx;\n setContext(ctx);\n\n if (onReady) onReady(ctx);\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.error('Failed to initialize GenesisGL:', err);\n if (onError) onError(err);\n }\n\n return () => {\n const { scene, viewport, webglCore } = contextRef.current;\n if (scene && webglCore) scene.dispose(webglCore);\n if (viewport) viewport.dispose();\n if (webglCore) webglCore.dispose();\n };\n }, []);\n\n return context;\n}\n","import { useRef, useEffect } from 'react';\nimport type { Renderer } from '../../../GenesisGL/src/Core/classes/Renderer';\n\nexport interface UseRendererOptions {\n renderer: Renderer | null;\n onFrame?: (time: number) => void;\n enabled?: boolean;\n}\n\n/**\n * Hook to manage the render loop with requestAnimationFrame.\n * Handles starting/stopping animation and cleanup.\n */\nexport function useRenderer(options: UseRendererOptions): void {\n const { renderer, onFrame, enabled = true } = options;\n const animationIdRef = useRef<number | null>(null);\n\n useEffect(() => {\n if (!renderer || !enabled) return;\n\n const animate = (time: number) => {\n if (onFrame) onFrame(time);\n animationIdRef.current = requestAnimationFrame(animate);\n };\n\n animationIdRef.current = requestAnimationFrame(animate);\n\n return () => {\n if (animationIdRef.current !== null) {\n cancelAnimationFrame(animationIdRef.current);\n }\n };\n }, [renderer, onFrame, enabled]);\n}\n","import { useState, useCallback } from 'react';\nimport type { GenesisGLContext } from './useGenesisGL';\n\nexport interface ScreenPoint {\n x: number;\n y: number;\n visible: boolean;\n}\n\nfunction mat4TransformVec4(m: Float32Array, x: number, y: number, z: number, w: number) {\n return {\n x: m[0] * x + m[4] * y + m[8] * z + m[12] * w,\n y: m[1] * x + m[5] * y + m[9] * z + m[13] * w,\n z: m[2] * x + m[6] * y + m[10] * z + m[14] * w,\n w: m[3] * x + m[7] * y + m[11] * z + m[15] * w,\n };\n}\n\nexport function useWorldToScreen(context: GenesisGLContext | null) {\n const [, forceUpdate] = useState(0);\n\n const project = useCallback(\n (worldX: number, worldY: number, worldZ: number): ScreenPoint => {\n if (!context?.viewport) return { x: 0, y: 0, visible: false };\n\n const { viewport } = context;\n const camera = viewport.camera;\n const canvas = viewport.getCanvas();\n\n const view = camera.getViewMatrix() as unknown as Float32Array;\n const proj = camera.getProjectionMatrix() as unknown as Float32Array;\n\n // view * worldPos\n const v = mat4TransformVec4(view, worldX, worldY, worldZ, 1);\n // proj * (view * worldPos)\n const c = mat4TransformVec4(proj, v.x, v.y, v.z, v.w);\n\n if (Math.abs(c.w) < 1e-6) return { x: 0, y: 0, visible: false };\n\n const ndcX = c.x / c.w;\n const ndcY = c.y / c.w;\n const ndcZ = c.z / c.w;\n\n const visible = ndcZ > -1 && ndcZ < 1 && Math.abs(ndcX) < 1.2 && Math.abs(ndcY) < 1.2;\n\n return {\n x: ((ndcX + 1) * 0.5) * canvas.clientWidth,\n y: ((1 - ndcY) * 0.5) * canvas.clientHeight,\n visible,\n };\n },\n [context],\n );\n\n // expose a tick function callers can plug into onFrame to re-render\n const tick = useCallback(() => forceUpdate((n) => n + 1), []);\n\n return { project, tick };\n}\n","import { useCallback, useState } from 'react';\nimport type { GenesisGLContext } from './useGenesisGL';\n\nexport interface CameraState {\n yaw: number;\n pitch: number;\n zoom: number;\n}\n\nexport interface UseCameraControlsResult {\n camera: CameraState;\n rotate: (deltaYaw: number, deltaPitch: number) => void;\n setYaw: (yaw: number) => void;\n setPitch: (pitch: number) => void;\n zoomBy: (delta: number) => void;\n setZoom: (zoom: number) => void;\n reset: () => void;\n}\n\nconst DEFAULT_YAW = 0;\nconst DEFAULT_PITCH = 0;\nconst DEFAULT_ZOOM = 1;\n\nexport function useCameraControls(\n ctx: GenesisGLContext | null,\n): UseCameraControlsResult {\n const [camera, setCameraState] = useState<CameraState>({\n yaw: DEFAULT_YAW,\n pitch: DEFAULT_PITCH,\n zoom: DEFAULT_ZOOM,\n });\n\n const sync = useCallback((c: { yaw: number; pitch: number; zoom: number }) => {\n setCameraState({ yaw: c.yaw, pitch: c.pitch, zoom: c.zoom });\n }, []);\n\n const rotate = useCallback(\n (deltaYaw: number, deltaPitch: number) => {\n const c = ctx?.viewport?.camera;\n if (!c) return;\n c.rotate(deltaYaw, deltaPitch);\n sync(c);\n },\n [ctx, sync],\n );\n\n const zoomBy = useCallback(\n (delta: number) => {\n const c = ctx?.viewport?.camera;\n if (!c) return;\n c.zoomBy(delta);\n sync(c);\n },\n [ctx, sync],\n );\n\n const setZoom = useCallback(\n (zoom: number) => {\n const c = ctx?.viewport?.camera;\n if (!c) return;\n c.setZoom(zoom);\n sync(c);\n },\n [ctx, sync],\n );\n\n const setYaw = useCallback(\n (yaw: number) => {\n const c = ctx?.viewport?.camera;\n if (!c) return;\n c.yaw = yaw;\n c.invalidate();\n sync(c);\n },\n [ctx, sync],\n );\n\n const setPitch = useCallback(\n (pitch: number) => {\n const c = ctx?.viewport?.camera;\n if (!c) return;\n c.pitch = pitch;\n c.invalidate();\n sync(c);\n },\n [ctx, sync],\n );\n\n const reset = useCallback(() => {\n const c = ctx?.viewport?.camera;\n if (!c) return;\n c.yaw = DEFAULT_YAW;\n c.pitch = DEFAULT_PITCH;\n c.invalidate();\n c.setZoom(DEFAULT_ZOOM);\n sync(c);\n }, [ctx, sync]);\n\n return { camera, rotate, setYaw, setPitch, zoomBy, setZoom, reset };\n}\n","import { useEffect, useRef } from 'react';\nimport { MouseDragControl } from '@fonsecabarreto/genesis-gl-core/Core';\nimport { useCameraControls } from './useCameraControls';\nimport type { GenesisGLContext } from './useGenesisGL';\n\nconst SENSITIVITY = 0.008;\n\nexport function useCameraMouseDrag(\n ctx: GenesisGLContext | null,\n canvasRef: React.RefObject<HTMLElement | null>,\n) {\n const { rotate } = useCameraControls(ctx);\n const rotateRef = useRef(rotate);\n rotateRef.current = rotate;\n\n useEffect(() => {\n const el = canvasRef.current;\n if (!el || !ctx) return;\n\n const drag = new MouseDragControl(el);\n\n const onDrag = (dx: number, dy: number, button: number) => {\n if (button !== 2) return;\n rotateRef.current(-dx * SENSITIVITY, dy * SENSITIVITY);\n };\n\n drag.onChange(onDrag);\n drag.enable();\n\n const onMouseDown = (e: MouseEvent) => {\n if (e.button !== 2) return;\n el.style.cursor = 'grabbing';\n };\n\n const onMouseUp = (e: MouseEvent) => {\n if (e.button !== 2) return;\n el.style.cursor = 'grab';\n };\n\n const onMouseEnter = () => {\n el.style.cursor = 'grab';\n };\n\n const onMouseLeave = () => {\n el.style.cursor = '';\n };\n\n const onContextMenu = (e: Event) => e.preventDefault();\n\n el.addEventListener('mousedown', onMouseDown);\n el.addEventListener('mouseenter', onMouseEnter);\n el.addEventListener('mouseleave', onMouseLeave);\n window.addEventListener('mouseup', onMouseUp);\n el.addEventListener('contextmenu', onContextMenu);\n\n return () => {\n drag.disable();\n el.removeEventListener('mousedown', onMouseDown);\n el.removeEventListener('mouseenter', onMouseEnter);\n el.removeEventListener('mouseleave', onMouseLeave);\n window.removeEventListener('mouseup', onMouseUp);\n el.removeEventListener('contextmenu', onContextMenu);\n el.style.cursor = '';\n };\n }, [ctx]);\n}\n"],"mappings":"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","names":["useRef","useEffect","useState","useCallback","useState","useEffect","useRef","useRef","useEffect"]}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCameraControls,
|
|
3
|
+
useCameraMouseDrag,
|
|
4
|
+
useGenesisGL,
|
|
5
|
+
useRenderer,
|
|
6
|
+
useWorldToScreen
|
|
7
|
+
} from "./chunk-QXHXGH2T.js";
|
|
8
|
+
|
|
9
|
+
// src/components/GenesisGLCanvas.tsx
|
|
10
|
+
import React, { useRef, useCallback } from "react";
|
|
11
|
+
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
var GenesisGLCanvas = React.forwardRef(
|
|
13
|
+
({
|
|
14
|
+
onReady,
|
|
15
|
+
onFrame,
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
initialYaw,
|
|
19
|
+
initialPitch,
|
|
20
|
+
initialZoom,
|
|
21
|
+
style,
|
|
22
|
+
className
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const canvasRef = useRef(null);
|
|
25
|
+
const setRef = useCallback(
|
|
26
|
+
(el) => {
|
|
27
|
+
canvasRef.current = el;
|
|
28
|
+
if (typeof ref === "function") ref(el);
|
|
29
|
+
else if (ref) ref.current = el;
|
|
30
|
+
},
|
|
31
|
+
[ref]
|
|
32
|
+
);
|
|
33
|
+
const onFrameRef = useRef(onFrame);
|
|
34
|
+
onFrameRef.current = onFrame;
|
|
35
|
+
const genesisContext = useGenesisGL({
|
|
36
|
+
canvasRef,
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
initialYaw,
|
|
40
|
+
initialPitch,
|
|
41
|
+
initialZoom,
|
|
42
|
+
onReady
|
|
43
|
+
});
|
|
44
|
+
const genesisContextRef = useRef(genesisContext);
|
|
45
|
+
genesisContextRef.current = genesisContext;
|
|
46
|
+
const stableOnFrame = useCallback((time) => {
|
|
47
|
+
const { renderer, scene } = genesisContextRef.current;
|
|
48
|
+
if (renderer && scene) {
|
|
49
|
+
renderer.render(scene);
|
|
50
|
+
onFrameRef.current?.(time);
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
useRenderer({ renderer: genesisContext.renderer, onFrame: stableOnFrame });
|
|
54
|
+
useCameraMouseDrag(genesisContext, canvasRef);
|
|
55
|
+
return /* @__PURE__ */ jsx(
|
|
56
|
+
"canvas",
|
|
57
|
+
{
|
|
58
|
+
ref: setRef,
|
|
59
|
+
style: { display: "block", width: "100%", height: "100%", ...style },
|
|
60
|
+
className
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
GenesisGLCanvas.displayName = "GenesisGLCanvas";
|
|
66
|
+
|
|
67
|
+
// src/components/RotationControls.tsx
|
|
68
|
+
import { useState } from "react";
|
|
69
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
70
|
+
var STEP = 0.05;
|
|
71
|
+
function IconBtn({
|
|
72
|
+
children,
|
|
73
|
+
title,
|
|
74
|
+
onClick,
|
|
75
|
+
color
|
|
76
|
+
}) {
|
|
77
|
+
const [active, setActive] = useState(false);
|
|
78
|
+
const [hover, setHover] = useState(false);
|
|
79
|
+
return /* @__PURE__ */ jsx2(
|
|
80
|
+
"button",
|
|
81
|
+
{
|
|
82
|
+
title,
|
|
83
|
+
onClick,
|
|
84
|
+
onMouseEnter: () => setHover(true),
|
|
85
|
+
onMouseLeave: () => {
|
|
86
|
+
setHover(false);
|
|
87
|
+
setActive(false);
|
|
88
|
+
},
|
|
89
|
+
onMouseDown: () => setActive(true),
|
|
90
|
+
onMouseUp: () => setActive(false),
|
|
91
|
+
style: {
|
|
92
|
+
display: "flex",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
justifyContent: "center",
|
|
95
|
+
width: "22px",
|
|
96
|
+
height: "22px",
|
|
97
|
+
border: `1px solid ${hover ? color + "88" : color + "33"}`,
|
|
98
|
+
borderRadius: "5px",
|
|
99
|
+
background: active ? color + "30" : hover ? color + "18" : color + "0c",
|
|
100
|
+
color: hover ? color : color + "aa",
|
|
101
|
+
cursor: "pointer",
|
|
102
|
+
fontSize: "11px",
|
|
103
|
+
fontWeight: 700,
|
|
104
|
+
lineHeight: 1,
|
|
105
|
+
padding: 0,
|
|
106
|
+
transition: "all 0.12s",
|
|
107
|
+
transform: active ? "scale(0.88)" : "scale(1)",
|
|
108
|
+
flexShrink: 0
|
|
109
|
+
},
|
|
110
|
+
children
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
function AxisPill({
|
|
115
|
+
label,
|
|
116
|
+
color,
|
|
117
|
+
value,
|
|
118
|
+
onDec,
|
|
119
|
+
onInc,
|
|
120
|
+
onReset
|
|
121
|
+
}) {
|
|
122
|
+
const deg = (value * 180 / Math.PI).toFixed(1);
|
|
123
|
+
const normalized = (value % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);
|
|
124
|
+
const pct = normalized / (Math.PI * 2) * 100;
|
|
125
|
+
return /* @__PURE__ */ jsxs(
|
|
126
|
+
"div",
|
|
127
|
+
{
|
|
128
|
+
style: {
|
|
129
|
+
display: "flex",
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
gap: "5px",
|
|
132
|
+
background: color + "0a",
|
|
133
|
+
border: `1px solid ${color}22`,
|
|
134
|
+
borderRadius: "8px",
|
|
135
|
+
padding: "5px 7px",
|
|
136
|
+
flex: 1,
|
|
137
|
+
minWidth: 0
|
|
138
|
+
},
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsxs("div", { style: { position: "relative", width: "18px", height: "18px", flexShrink: 0 }, children: [
|
|
141
|
+
/* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", children: [
|
|
142
|
+
/* @__PURE__ */ jsx2("circle", { cx: "9", cy: "9", r: "7", fill: "none", stroke: color + "22", strokeWidth: "2" }),
|
|
143
|
+
/* @__PURE__ */ jsx2(
|
|
144
|
+
"circle",
|
|
145
|
+
{
|
|
146
|
+
cx: "9",
|
|
147
|
+
cy: "9",
|
|
148
|
+
r: "7",
|
|
149
|
+
fill: "none",
|
|
150
|
+
stroke: color,
|
|
151
|
+
strokeWidth: "2",
|
|
152
|
+
strokeDasharray: `${pct / 100 * 44} 44`,
|
|
153
|
+
strokeLinecap: "round",
|
|
154
|
+
transform: "rotate(-90 9 9)",
|
|
155
|
+
style: { transition: "stroke-dasharray 0.1s ease" }
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
] }),
|
|
159
|
+
/* @__PURE__ */ jsx2(
|
|
160
|
+
"span",
|
|
161
|
+
{
|
|
162
|
+
style: {
|
|
163
|
+
position: "absolute",
|
|
164
|
+
inset: 0,
|
|
165
|
+
display: "flex",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
justifyContent: "center",
|
|
168
|
+
fontSize: "7px",
|
|
169
|
+
fontWeight: 800,
|
|
170
|
+
color,
|
|
171
|
+
letterSpacing: "-0.02em"
|
|
172
|
+
},
|
|
173
|
+
children: label
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
] }),
|
|
177
|
+
/* @__PURE__ */ jsxs(
|
|
178
|
+
"span",
|
|
179
|
+
{
|
|
180
|
+
style: {
|
|
181
|
+
fontSize: "10px",
|
|
182
|
+
color: color + "cc",
|
|
183
|
+
fontVariantNumeric: "tabular-nums",
|
|
184
|
+
width: "36px",
|
|
185
|
+
textAlign: "right",
|
|
186
|
+
flexShrink: 0
|
|
187
|
+
},
|
|
188
|
+
children: [
|
|
189
|
+
deg,
|
|
190
|
+
"\xB0"
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "3px", marginLeft: "2px" }, children: [
|
|
195
|
+
/* @__PURE__ */ jsx2(IconBtn, { color, title: `\u2212${label}`, onClick: onDec, children: "\u2212" }),
|
|
196
|
+
/* @__PURE__ */ jsx2(IconBtn, { color, title: `+${label}`, onClick: onInc, children: "+" }),
|
|
197
|
+
/* @__PURE__ */ jsx2(IconBtn, { color: color + "88", title: "Reset", onClick: onReset, children: "\u21BA" })
|
|
198
|
+
] })
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function RotationControls({ context, style }) {
|
|
204
|
+
const { camera, rotate, setYaw, setPitch, reset } = useCameraControls(context);
|
|
205
|
+
return /* @__PURE__ */ jsxs(
|
|
206
|
+
"div",
|
|
207
|
+
{
|
|
208
|
+
style: {
|
|
209
|
+
display: "flex",
|
|
210
|
+
alignItems: "center",
|
|
211
|
+
gap: "6px",
|
|
212
|
+
background: "rgba(8,8,12,0.78)",
|
|
213
|
+
border: "1px solid rgba(255,255,255,0.07)",
|
|
214
|
+
borderRadius: "12px",
|
|
215
|
+
padding: "6px 8px",
|
|
216
|
+
backdropFilter: "blur(16px)",
|
|
217
|
+
boxShadow: "0 4px 24px rgba(0,0,0,0.5)",
|
|
218
|
+
fontFamily: '"Inter", system-ui, monospace',
|
|
219
|
+
...style
|
|
220
|
+
},
|
|
221
|
+
children: [
|
|
222
|
+
/* @__PURE__ */ jsx2(
|
|
223
|
+
"div",
|
|
224
|
+
{
|
|
225
|
+
title: context ? "Camera ready" : "Waiting for context\u2026",
|
|
226
|
+
style: {
|
|
227
|
+
width: "6px",
|
|
228
|
+
height: "6px",
|
|
229
|
+
borderRadius: "50%",
|
|
230
|
+
background: context ? "#c3e88d" : "#555",
|
|
231
|
+
boxShadow: context ? "0 0 6px #c3e88daa" : "none",
|
|
232
|
+
flexShrink: 0,
|
|
233
|
+
transition: "all 0.3s"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
/* @__PURE__ */ jsx2(
|
|
238
|
+
AxisPill,
|
|
239
|
+
{
|
|
240
|
+
label: "Yaw",
|
|
241
|
+
color: "#ff5370",
|
|
242
|
+
value: camera.yaw,
|
|
243
|
+
onDec: () => rotate(-STEP, 0),
|
|
244
|
+
onInc: () => rotate(STEP, 0),
|
|
245
|
+
onReset: () => setYaw(0)
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
/* @__PURE__ */ jsx2(
|
|
249
|
+
AxisPill,
|
|
250
|
+
{
|
|
251
|
+
label: "Pitch",
|
|
252
|
+
color: "#82aaff",
|
|
253
|
+
value: camera.pitch,
|
|
254
|
+
onDec: () => rotate(0, -STEP),
|
|
255
|
+
onInc: () => rotate(0, STEP),
|
|
256
|
+
onReset: () => setPitch(0)
|
|
257
|
+
}
|
|
258
|
+
),
|
|
259
|
+
/* @__PURE__ */ jsx2(IconBtn, { color: "#ffffff", title: "Reset all", onClick: reset, children: "\u2298" })
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/components/FaceLabel.tsx
|
|
266
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
267
|
+
function transformLocalToWorld(m, x, y, z) {
|
|
268
|
+
return {
|
|
269
|
+
x: m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
270
|
+
y: m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
271
|
+
z: m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function cross(ax, ay, az, bx, by, bz) {
|
|
275
|
+
return {
|
|
276
|
+
x: ay * bz - az * by,
|
|
277
|
+
y: az * bx - ax * bz,
|
|
278
|
+
z: ax * by - ay * bx
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function solveHomography(w, h, p) {
|
|
282
|
+
const [p0, p1, p2, p3] = p;
|
|
283
|
+
const sx = [0, w, w, 0];
|
|
284
|
+
const sy = [0, 0, h, h];
|
|
285
|
+
const dx = [p0.x, p1.x, p2.x, p3.x];
|
|
286
|
+
const dy = [p0.y, p1.y, p2.y, p3.y];
|
|
287
|
+
const M = [];
|
|
288
|
+
for (let i = 0; i < 4; i++) {
|
|
289
|
+
M.push([sx[i], sy[i], 1, 0, 0, 0, -dx[i] * sx[i], -dx[i] * sy[i], dx[i]]);
|
|
290
|
+
M.push([0, 0, 0, sx[i], sy[i], 1, -dy[i] * sx[i], -dy[i] * sy[i], dy[i]]);
|
|
291
|
+
}
|
|
292
|
+
const n = 8;
|
|
293
|
+
for (let col = 0; col < n; col++) {
|
|
294
|
+
let maxRow = col;
|
|
295
|
+
for (let row = col + 1; row < n; row++) {
|
|
296
|
+
if (Math.abs(M[row][col]) > Math.abs(M[maxRow][col])) maxRow = row;
|
|
297
|
+
}
|
|
298
|
+
[M[col], M[maxRow]] = [M[maxRow], M[col]];
|
|
299
|
+
if (Math.abs(M[col][col]) < 1e-10) return null;
|
|
300
|
+
const pivot = M[col][col];
|
|
301
|
+
for (let k = col; k <= n; k++) M[col][k] /= pivot;
|
|
302
|
+
for (let row = 0; row < n; row++) {
|
|
303
|
+
if (row === col) continue;
|
|
304
|
+
const f = M[row][col];
|
|
305
|
+
for (let k = col; k <= n; k++) M[row][k] -= f * M[col][k];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return M.map((row) => row[n]);
|
|
309
|
+
}
|
|
310
|
+
function buildMatrix3d(w, h, p) {
|
|
311
|
+
const hv = solveHomography(w, h, p);
|
|
312
|
+
if (!hv) return "none";
|
|
313
|
+
const [h00, h01, h02, h10, h11, h12, h20, h21] = hv;
|
|
314
|
+
return `matrix3d(${h00},${h10},0,${h20},${h01},${h11},0,${h21},0,0,1,0,${h02},${h12},0,1)`;
|
|
315
|
+
}
|
|
316
|
+
function FaceSkin({
|
|
317
|
+
context,
|
|
318
|
+
model,
|
|
319
|
+
corners,
|
|
320
|
+
children,
|
|
321
|
+
style,
|
|
322
|
+
className,
|
|
323
|
+
surfaceWidth = 300,
|
|
324
|
+
surfaceHeight = 200
|
|
325
|
+
}) {
|
|
326
|
+
const { project } = useWorldToScreen(context);
|
|
327
|
+
if (!model || !context) return null;
|
|
328
|
+
const mm = model.getModelMatrix();
|
|
329
|
+
const worldPts = corners.map(
|
|
330
|
+
([lx, ly, lz]) => transformLocalToWorld(mm, lx, ly, lz)
|
|
331
|
+
);
|
|
332
|
+
const e1x = worldPts[1].x - worldPts[0].x;
|
|
333
|
+
const e1y = worldPts[1].y - worldPts[0].y;
|
|
334
|
+
const e1z = worldPts[1].z - worldPts[0].z;
|
|
335
|
+
const e2x = worldPts[3].x - worldPts[0].x;
|
|
336
|
+
const e2y = worldPts[3].y - worldPts[0].y;
|
|
337
|
+
const e2z = worldPts[3].z - worldPts[0].z;
|
|
338
|
+
const normal = cross(e1x, e1y, e1z, e2x, e2y, e2z);
|
|
339
|
+
const camera = context.viewport.camera;
|
|
340
|
+
const radius = Math.sqrt(
|
|
341
|
+
(camera.position[0] - camera.target[0]) ** 2 + (camera.position[1] - camera.target[1]) ** 2 + (camera.position[2] - camera.target[2]) ** 2
|
|
342
|
+
);
|
|
343
|
+
const eyeX = camera.target[0] + radius * Math.cos(camera.pitch) * Math.sin(camera.yaw);
|
|
344
|
+
const eyeY = camera.target[1] + radius * Math.sin(camera.pitch);
|
|
345
|
+
const eyeZ = camera.target[2] + radius * Math.cos(camera.pitch) * Math.cos(camera.yaw);
|
|
346
|
+
const toCamera = {
|
|
347
|
+
x: eyeX - worldPts[0].x,
|
|
348
|
+
y: eyeY - worldPts[0].y,
|
|
349
|
+
z: eyeZ - worldPts[0].z
|
|
350
|
+
};
|
|
351
|
+
const dot = normal.x * toCamera.x + normal.y * toCamera.y + normal.z * toCamera.z;
|
|
352
|
+
if (dot <= 0) return null;
|
|
353
|
+
const screenPts = worldPts.map((w) => project(w.x, w.y, w.z));
|
|
354
|
+
if (screenPts.some((pt) => !pt.visible)) return null;
|
|
355
|
+
const remapped = [screenPts[3], screenPts[2], screenPts[1], screenPts[0]];
|
|
356
|
+
const matrix = buildMatrix3d(surfaceWidth, surfaceHeight, remapped);
|
|
357
|
+
return /* @__PURE__ */ jsx3(
|
|
358
|
+
"div",
|
|
359
|
+
{
|
|
360
|
+
style: {
|
|
361
|
+
position: "absolute",
|
|
362
|
+
inset: 0,
|
|
363
|
+
pointerEvents: "none",
|
|
364
|
+
overflow: "hidden"
|
|
365
|
+
},
|
|
366
|
+
children: /* @__PURE__ */ jsx3(
|
|
367
|
+
"div",
|
|
368
|
+
{
|
|
369
|
+
className,
|
|
370
|
+
style: {
|
|
371
|
+
position: "absolute",
|
|
372
|
+
top: 0,
|
|
373
|
+
left: 0,
|
|
374
|
+
width: surfaceWidth,
|
|
375
|
+
height: surfaceHeight,
|
|
376
|
+
transformOrigin: "0 0",
|
|
377
|
+
transform: matrix,
|
|
378
|
+
overflow: "hidden",
|
|
379
|
+
pointerEvents: "auto",
|
|
380
|
+
...style
|
|
381
|
+
},
|
|
382
|
+
children
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export {
|
|
390
|
+
GenesisGLCanvas,
|
|
391
|
+
RotationControls,
|
|
392
|
+
FaceSkin
|
|
393
|
+
};
|
|
394
|
+
//# sourceMappingURL=chunk-PJM4LJXC.js.map
|