@fonsecabarreto/genesis-gl-react 0.1.33 → 0.1.34

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.
@@ -0,0 +1,825 @@
1
+ import {
2
+ useCameraControls,
3
+ useGenesisGL,
4
+ useRenderer,
5
+ useWorldToScreen
6
+ } from "./chunk-7AGC27WM.js";
7
+
8
+ // src/components/GenesisGLCanvas.tsx
9
+ import React, { useRef, useCallback, useEffect } from "react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ function ResetButton({ onClick }) {
12
+ return /* @__PURE__ */ jsx(
13
+ "button",
14
+ {
15
+ onClick,
16
+ title: "Reset camera",
17
+ style: {
18
+ position: "absolute",
19
+ top: 12,
20
+ left: 12,
21
+ width: 32,
22
+ height: 32,
23
+ borderRadius: 8,
24
+ border: "1px solid rgba(255,255,255,0.1)",
25
+ background: "rgba(8,8,14,0.6)",
26
+ backdropFilter: "blur(8px)",
27
+ color: "rgba(255,255,255,0.6)",
28
+ fontSize: 15,
29
+ cursor: "pointer",
30
+ display: "flex",
31
+ alignItems: "center",
32
+ justifyContent: "center",
33
+ lineHeight: 1,
34
+ padding: 0,
35
+ transition: "background 0.15s, color 0.15s",
36
+ zIndex: 10
37
+ },
38
+ onMouseEnter: (e) => {
39
+ e.currentTarget.style.background = "rgba(255,255,255,0.12)";
40
+ e.currentTarget.style.color = "#fff";
41
+ },
42
+ onMouseLeave: (e) => {
43
+ e.currentTarget.style.background = "rgba(8,8,14,0.6)";
44
+ e.currentTarget.style.color = "rgba(255,255,255,0.6)";
45
+ },
46
+ children: "\u2298"
47
+ }
48
+ );
49
+ }
50
+ function CanvasInner({
51
+ setRef,
52
+ style,
53
+ className,
54
+ onDoubleClick
55
+ }) {
56
+ return /* @__PURE__ */ jsx(
57
+ "canvas",
58
+ {
59
+ ref: setRef,
60
+ onDoubleClick,
61
+ style: { display: "block", width: "100%", height: "100%", ...style },
62
+ className
63
+ }
64
+ );
65
+ }
66
+ function GenesisGLCanvasInner({
67
+ context,
68
+ setRef,
69
+ initialYaw,
70
+ initialPitch,
71
+ initialZoom,
72
+ background,
73
+ style,
74
+ className,
75
+ onFrame
76
+ }) {
77
+ const { reset, setYaw, setPitch, setZoom } = useCameraControls(context);
78
+ const contextRef = useRef(context);
79
+ contextRef.current = context;
80
+ const onFrameRef = useRef(onFrame);
81
+ onFrameRef.current = onFrame;
82
+ const stableOnFrame = useCallback((time) => {
83
+ const { renderer, scene } = contextRef.current;
84
+ if (renderer && scene) {
85
+ renderer.render(scene);
86
+ onFrameRef.current?.(time);
87
+ }
88
+ }, []);
89
+ useEffect(() => {
90
+ if (context.renderer && background) {
91
+ context.renderer.clearColor = background;
92
+ }
93
+ }, [context.renderer, background]);
94
+ useRenderer({ renderer: context.renderer, onFrame: stableOnFrame });
95
+ const handleReset = useCallback(() => {
96
+ if (initialYaw !== void 0) setYaw(initialYaw);
97
+ else reset();
98
+ if (initialPitch !== void 0) setPitch(initialPitch);
99
+ if (initialZoom !== void 0) setZoom(initialZoom);
100
+ }, [reset, setYaw, setPitch, setZoom, initialYaw, initialPitch, initialZoom]);
101
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ /* @__PURE__ */ jsx(ResetButton, { onClick: handleReset }),
103
+ /* @__PURE__ */ jsx(
104
+ CanvasInner,
105
+ {
106
+ setRef,
107
+ style,
108
+ className,
109
+ onDoubleClick: handleReset
110
+ }
111
+ )
112
+ ] });
113
+ }
114
+ var GenesisGLCanvas = React.forwardRef(
115
+ ({
116
+ onReady,
117
+ onFrame,
118
+ width,
119
+ height,
120
+ initialYaw,
121
+ initialPitch,
122
+ initialZoom,
123
+ background,
124
+ style,
125
+ className
126
+ }, ref) => {
127
+ const canvasRef = useRef(null);
128
+ const setRef = useCallback(
129
+ (el) => {
130
+ canvasRef.current = el;
131
+ if (typeof ref === "function") ref(el);
132
+ else if (ref) ref.current = el;
133
+ },
134
+ [ref]
135
+ );
136
+ const genesisContext = useGenesisGL({
137
+ canvasRef,
138
+ width,
139
+ height,
140
+ initialYaw,
141
+ initialPitch,
142
+ initialZoom,
143
+ onReady
144
+ });
145
+ if (!genesisContext.renderer) {
146
+ return /* @__PURE__ */ jsx(
147
+ "canvas",
148
+ {
149
+ ref: setRef,
150
+ style: { display: "block", width: "100%", height: "100%", ...style },
151
+ className
152
+ }
153
+ );
154
+ }
155
+ return /* @__PURE__ */ jsx(
156
+ GenesisGLCanvasInner,
157
+ {
158
+ context: genesisContext,
159
+ setRef,
160
+ initialYaw,
161
+ initialPitch,
162
+ initialZoom,
163
+ background,
164
+ style,
165
+ className,
166
+ onFrame
167
+ }
168
+ );
169
+ }
170
+ );
171
+ GenesisGLCanvas.displayName = "GenesisGLCanvas";
172
+
173
+ // src/components/ViewportGizmo.tsx
174
+ import { useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
175
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
176
+ var FACES = [
177
+ { label: "+X", dir: [1, 0, 0], color: "#ff5370" },
178
+ { label: "-X", dir: [-1, 0, 0], color: "#ff5370", dim: true },
179
+ { label: "+Y", dir: [0, 1, 0], color: "#c3e88d" },
180
+ { label: "-Y", dir: [0, -1, 0], color: "#c3e88d", dim: true },
181
+ { label: "+Z", dir: [0, 0, 1], color: "#82aaff" },
182
+ { label: "-Z", dir: [0, 0, -1], color: "#82aaff", dim: true }
183
+ ];
184
+ var SNAP_PITCH = {
185
+ "+Y": Math.PI / 2 - 0.01,
186
+ "-Y": -(Math.PI / 2 - 0.01)
187
+ };
188
+ function project(dir, yaw, pitch, radius) {
189
+ const [dx, dy, dz] = dir;
190
+ const cosY = Math.cos(-yaw);
191
+ const sinY = Math.sin(-yaw);
192
+ const rx = dx * cosY + dz * sinY;
193
+ const ry = dy;
194
+ const rz = -dx * sinY + dz * cosY;
195
+ const cosP = Math.cos(-pitch);
196
+ const sinP = Math.sin(-pitch);
197
+ const fx = rx;
198
+ const fy = ry * cosP - rz * sinP;
199
+ const fz = ry * sinP + rz * cosP;
200
+ return { x: fx * radius, y: -fy * radius, z: fz };
201
+ }
202
+ var DRAG_SENSITIVITY = 8e-3;
203
+ function ViewportGizmo({
204
+ context,
205
+ size = 96,
206
+ position = "top-right",
207
+ style
208
+ }) {
209
+ const { camera, setYaw, setPitch, rotate } = useCameraControls(context);
210
+ const dragging = useRef2(false);
211
+ const lastPos = useRef2({ x: 0, y: 0 });
212
+ const containerRef = useRef2(null);
213
+ const onMouseDown = useCallback2((e) => {
214
+ if (e.button !== 0) return;
215
+ dragging.current = true;
216
+ lastPos.current = { x: e.clientX, y: e.clientY };
217
+ e.stopPropagation();
218
+ e.preventDefault();
219
+ }, []);
220
+ useEffect2(() => {
221
+ const onMove = (e) => {
222
+ if (!dragging.current) return;
223
+ const dx = e.clientX - lastPos.current.x;
224
+ const dy = e.clientY - lastPos.current.y;
225
+ lastPos.current = { x: e.clientX, y: e.clientY };
226
+ rotate(-dx * DRAG_SENSITIVITY, -dy * DRAG_SENSITIVITY);
227
+ };
228
+ const onUp = () => {
229
+ dragging.current = false;
230
+ };
231
+ window.addEventListener("mousemove", onMove);
232
+ window.addEventListener("mouseup", onUp);
233
+ return () => {
234
+ window.removeEventListener("mousemove", onMove);
235
+ window.removeEventListener("mouseup", onUp);
236
+ };
237
+ }, [rotate]);
238
+ const snapTo = useCallback2((face) => {
239
+ const [dx, , dz] = face.dir;
240
+ const targetYaw = Math.atan2(dx, dz);
241
+ const targetPitch = SNAP_PITCH[face.label] ?? 0;
242
+ setYaw(targetYaw);
243
+ setPitch(targetPitch);
244
+ }, [setYaw, setPitch]);
245
+ const half = size / 2;
246
+ const dotRadius = size * 0.14;
247
+ const orbitRadius = half * 0.62;
248
+ const projected = FACES.map((face) => ({
249
+ ...face,
250
+ ...project(face.dir, camera.yaw, camera.pitch, orbitRadius)
251
+ })).sort((a, b) => a.z - b.z);
252
+ const posStyle = position === "top-right" ? { top: 12, right: 12 } : position === "top-left" ? { top: 12, left: 12 } : position === "bottom-right" ? { bottom: 12, right: 12 } : { bottom: 12, left: 12 };
253
+ return /* @__PURE__ */ jsx2(
254
+ "div",
255
+ {
256
+ ref: containerRef,
257
+ onMouseDown,
258
+ style: {
259
+ position: "absolute",
260
+ width: size,
261
+ height: size,
262
+ borderRadius: "50%",
263
+ background: "rgba(8,8,14,0.55)",
264
+ border: "1px solid rgba(255,255,255,0.07)",
265
+ backdropFilter: "blur(8px)",
266
+ cursor: dragging.current ? "grabbing" : "grab",
267
+ userSelect: "none",
268
+ ...posStyle,
269
+ ...style
270
+ },
271
+ children: /* @__PURE__ */ jsxs2(
272
+ "svg",
273
+ {
274
+ width: size,
275
+ height: size,
276
+ viewBox: `${-half} ${-half} ${size} ${size}`,
277
+ style: { overflow: "visible", display: "block" },
278
+ children: [
279
+ ["x", "y", "z"].map((axis) => {
280
+ const pos = projected.find((f) => f.label === `+${axis.toUpperCase()}`);
281
+ const neg = projected.find((f) => f.label === `-${axis.toUpperCase()}`);
282
+ if (!pos || !neg) return null;
283
+ return /* @__PURE__ */ jsx2(
284
+ "line",
285
+ {
286
+ x1: neg.x,
287
+ y1: neg.y,
288
+ x2: pos.x,
289
+ y2: pos.y,
290
+ stroke: pos.color,
291
+ strokeWidth: 1.5,
292
+ strokeOpacity: 0.35
293
+ },
294
+ axis
295
+ );
296
+ }),
297
+ projected.map((face) => {
298
+ const isFront = face.z >= 0;
299
+ const opacity = isFront ? 1 : 0.28;
300
+ const r = isFront ? dotRadius : dotRadius * 0.75;
301
+ return /* @__PURE__ */ jsxs2(
302
+ "g",
303
+ {
304
+ style: { cursor: "pointer" },
305
+ onClick: (e) => {
306
+ e.stopPropagation();
307
+ snapTo(face);
308
+ },
309
+ children: [
310
+ /* @__PURE__ */ jsx2(
311
+ "circle",
312
+ {
313
+ cx: face.x,
314
+ cy: face.y,
315
+ r,
316
+ fill: face.color,
317
+ fillOpacity: opacity * (face.dim ? 0.6 : 1),
318
+ stroke: face.color,
319
+ strokeWidth: isFront ? 1.5 : 0.5,
320
+ strokeOpacity: opacity * 0.6
321
+ }
322
+ ),
323
+ isFront && /* @__PURE__ */ jsx2(
324
+ "text",
325
+ {
326
+ x: face.x,
327
+ y: face.y,
328
+ textAnchor: "middle",
329
+ dominantBaseline: "central",
330
+ fontSize: r * 0.75,
331
+ fontWeight: 700,
332
+ fontFamily: "system-ui, monospace",
333
+ fill: "#fff",
334
+ fillOpacity: 0.9,
335
+ style: { pointerEvents: "none" },
336
+ children: face.label
337
+ }
338
+ )
339
+ ]
340
+ },
341
+ face.label
342
+ );
343
+ })
344
+ ]
345
+ }
346
+ )
347
+ }
348
+ );
349
+ }
350
+
351
+ // src/components/OrbitArrows.tsx
352
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3 } from "react";
353
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
354
+ var DRAG_SENSITIVITY2 = 8e-3;
355
+ var ARC_GAP = 0.18;
356
+ function sampleArc(cx, cy, cz, radius, segments, plane, angleOffset = 0) {
357
+ const pts = [];
358
+ const total = Math.PI * 2 * (1 - ARC_GAP);
359
+ for (let i = 0; i <= segments; i++) {
360
+ const t = angleOffset + i / segments * total;
361
+ const cos = Math.cos(t);
362
+ const sin = Math.sin(t);
363
+ if (plane === "xz") pts.push([cx + cos * radius, cy, cz + sin * radius]);
364
+ else if (plane === "yz") pts.push([cx, cy + cos * radius, cz + sin * radius]);
365
+ else pts.push([cx + cos * radius, cy + sin * radius, cz]);
366
+ }
367
+ return pts;
368
+ }
369
+ function arrowHead(tip, prev, size) {
370
+ const dx = tip.x - prev.x;
371
+ const dy = tip.y - prev.y;
372
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
373
+ const ux = dx / len;
374
+ const uy = dy / len;
375
+ const px = -uy;
376
+ const py = ux;
377
+ const base = { x: tip.x - ux * size, y: tip.y - uy * size };
378
+ return [
379
+ `${tip.x},${tip.y}`,
380
+ `${base.x + px * size * 0.45},${base.y + py * size * 0.45}`,
381
+ `${base.x - px * size * 0.45},${base.y - py * size * 0.45}`
382
+ ].join(" ");
383
+ }
384
+ var ARCS = [
385
+ { plane: "xz", color: "#c3e88d", dragAxis: [-1, 0], angleOffset: Math.PI * 0.6 },
386
+ { plane: "yz", color: "#82aaff", dragAxis: [0, -1], angleOffset: Math.PI * 0.1 }
387
+ ];
388
+ function OrbitArrows({
389
+ context,
390
+ center = [0, 0, 0],
391
+ radius = 1.2,
392
+ segments = 48,
393
+ style
394
+ }) {
395
+ const { rotate } = useCameraControls(context);
396
+ const { project: project2 } = useWorldToScreen(context);
397
+ const dragging = useRef3(null);
398
+ const rotateRef = useRef3(rotate);
399
+ rotateRef.current = rotate;
400
+ useEffect3(() => {
401
+ const onMove = (e) => {
402
+ if (!dragging.current) return;
403
+ const dx = e.clientX - dragging.current.lastX;
404
+ const dy = e.clientY - dragging.current.lastY;
405
+ dragging.current.lastX = e.clientX;
406
+ dragging.current.lastY = e.clientY;
407
+ const arc = ARCS[dragging.current.arcIndex];
408
+ rotateRef.current(
409
+ dx * DRAG_SENSITIVITY2 * arc.dragAxis[0],
410
+ dy * DRAG_SENSITIVITY2 * arc.dragAxis[1]
411
+ );
412
+ };
413
+ const onUp = () => {
414
+ dragging.current = null;
415
+ };
416
+ window.addEventListener("mousemove", onMove);
417
+ window.addEventListener("mouseup", onUp);
418
+ return () => {
419
+ window.removeEventListener("mousemove", onMove);
420
+ window.removeEventListener("mouseup", onUp);
421
+ };
422
+ }, []);
423
+ const onArcMouseDown = useCallback3((arcIndex, e) => {
424
+ e.stopPropagation();
425
+ e.preventDefault();
426
+ dragging.current = { arcIndex, lastX: e.clientX, lastY: e.clientY };
427
+ }, []);
428
+ if (!context?.viewport) return null;
429
+ const canvas = context.viewport.getCanvas();
430
+ const W = canvas.clientWidth;
431
+ const H = canvas.clientHeight;
432
+ const [cx, cy, cz] = center;
433
+ return /* @__PURE__ */ jsx3(
434
+ "svg",
435
+ {
436
+ style: {
437
+ position: "absolute",
438
+ inset: 0,
439
+ width: "100%",
440
+ height: "100%",
441
+ pointerEvents: "none",
442
+ overflow: "visible",
443
+ ...style
444
+ },
445
+ viewBox: `0 0 ${W} ${H}`,
446
+ children: ARCS.map((arc, i) => {
447
+ const worldPts = sampleArc(cx, cy, cz, radius, segments, arc.plane, arc.angleOffset);
448
+ const screenPts = worldPts.map((p) => project2(p[0], p[1], p[2]));
449
+ if (screenPts.some((p) => !p.visible)) return null;
450
+ const polyline = screenPts.map((p) => `${p.x},${p.y}`).join(" ");
451
+ const tip = screenPts[screenPts.length - 1];
452
+ const prev = screenPts[screenPts.length - 4];
453
+ const arrowSize = 10;
454
+ const arrowPoly = arrowHead(tip, prev, arrowSize);
455
+ return /* @__PURE__ */ jsxs3("g", { style: { pointerEvents: "auto", cursor: "grab" }, children: [
456
+ /* @__PURE__ */ jsx3(
457
+ "polyline",
458
+ {
459
+ points: polyline,
460
+ fill: "none",
461
+ stroke: "transparent",
462
+ strokeWidth: 14,
463
+ onMouseDown: (e) => onArcMouseDown(i, e),
464
+ style: { cursor: "grab" }
465
+ }
466
+ ),
467
+ /* @__PURE__ */ jsx3(
468
+ "polyline",
469
+ {
470
+ points: polyline,
471
+ fill: "none",
472
+ stroke: arc.color,
473
+ strokeWidth: 2.5,
474
+ strokeOpacity: 0.75,
475
+ strokeLinecap: "round",
476
+ style: { pointerEvents: "none" }
477
+ }
478
+ ),
479
+ /* @__PURE__ */ jsx3(
480
+ "polygon",
481
+ {
482
+ points: arrowPoly,
483
+ fill: arc.color,
484
+ fillOpacity: 0.9,
485
+ style: { pointerEvents: "none" }
486
+ }
487
+ )
488
+ ] }, i);
489
+ })
490
+ }
491
+ );
492
+ }
493
+
494
+ // src/components/RotationControls.tsx
495
+ import { useState } from "react";
496
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
497
+ var STEP = 0.05;
498
+ function IconBtn({
499
+ children,
500
+ title,
501
+ onClick,
502
+ color
503
+ }) {
504
+ const [active, setActive] = useState(false);
505
+ const [hover, setHover] = useState(false);
506
+ return /* @__PURE__ */ jsx4(
507
+ "button",
508
+ {
509
+ title,
510
+ onClick,
511
+ onMouseEnter: () => setHover(true),
512
+ onMouseLeave: () => {
513
+ setHover(false);
514
+ setActive(false);
515
+ },
516
+ onMouseDown: () => setActive(true),
517
+ onMouseUp: () => setActive(false),
518
+ style: {
519
+ display: "flex",
520
+ alignItems: "center",
521
+ justifyContent: "center",
522
+ width: "22px",
523
+ height: "22px",
524
+ border: `1px solid ${hover ? color + "88" : color + "33"}`,
525
+ borderRadius: "5px",
526
+ background: active ? color + "30" : hover ? color + "18" : color + "0c",
527
+ color: hover ? color : color + "aa",
528
+ cursor: "pointer",
529
+ fontSize: "11px",
530
+ fontWeight: 700,
531
+ lineHeight: 1,
532
+ padding: 0,
533
+ transition: "all 0.12s",
534
+ transform: active ? "scale(0.88)" : "scale(1)",
535
+ flexShrink: 0
536
+ },
537
+ children
538
+ }
539
+ );
540
+ }
541
+ function AxisPill({
542
+ label,
543
+ color,
544
+ value,
545
+ onDec,
546
+ onInc,
547
+ onReset
548
+ }) {
549
+ const deg = (value * 180 / Math.PI).toFixed(1);
550
+ const normalized = (value % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);
551
+ const pct = normalized / (Math.PI * 2) * 100;
552
+ return /* @__PURE__ */ jsxs4(
553
+ "div",
554
+ {
555
+ style: {
556
+ display: "flex",
557
+ alignItems: "center",
558
+ gap: "5px",
559
+ background: color + "0a",
560
+ border: `1px solid ${color}22`,
561
+ borderRadius: "8px",
562
+ padding: "5px 7px",
563
+ flex: 1,
564
+ minWidth: 0
565
+ },
566
+ children: [
567
+ /* @__PURE__ */ jsxs4("div", { style: { position: "relative", width: "18px", height: "18px", flexShrink: 0 }, children: [
568
+ /* @__PURE__ */ jsxs4("svg", { width: "18", height: "18", viewBox: "0 0 18 18", children: [
569
+ /* @__PURE__ */ jsx4("circle", { cx: "9", cy: "9", r: "7", fill: "none", stroke: color + "22", strokeWidth: "2" }),
570
+ /* @__PURE__ */ jsx4(
571
+ "circle",
572
+ {
573
+ cx: "9",
574
+ cy: "9",
575
+ r: "7",
576
+ fill: "none",
577
+ stroke: color,
578
+ strokeWidth: "2",
579
+ strokeDasharray: `${pct / 100 * 44} 44`,
580
+ strokeLinecap: "round",
581
+ transform: "rotate(-90 9 9)",
582
+ style: { transition: "stroke-dasharray 0.1s ease" }
583
+ }
584
+ )
585
+ ] }),
586
+ /* @__PURE__ */ jsx4(
587
+ "span",
588
+ {
589
+ style: {
590
+ position: "absolute",
591
+ inset: 0,
592
+ display: "flex",
593
+ alignItems: "center",
594
+ justifyContent: "center",
595
+ fontSize: "7px",
596
+ fontWeight: 800,
597
+ color,
598
+ letterSpacing: "-0.02em"
599
+ },
600
+ children: label
601
+ }
602
+ )
603
+ ] }),
604
+ /* @__PURE__ */ jsxs4(
605
+ "span",
606
+ {
607
+ style: {
608
+ fontSize: "10px",
609
+ color: color + "cc",
610
+ fontVariantNumeric: "tabular-nums",
611
+ width: "36px",
612
+ textAlign: "right",
613
+ flexShrink: 0
614
+ },
615
+ children: [
616
+ deg,
617
+ "\xB0"
618
+ ]
619
+ }
620
+ ),
621
+ /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: "3px", marginLeft: "2px" }, children: [
622
+ /* @__PURE__ */ jsx4(IconBtn, { color, title: `\u2212${label}`, onClick: onDec, children: "\u2212" }),
623
+ /* @__PURE__ */ jsx4(IconBtn, { color, title: `+${label}`, onClick: onInc, children: "+" }),
624
+ /* @__PURE__ */ jsx4(IconBtn, { color: color + "88", title: "Reset", onClick: onReset, children: "\u21BA" })
625
+ ] })
626
+ ]
627
+ }
628
+ );
629
+ }
630
+ function RotationControls({ context, style }) {
631
+ const { camera, rotate, setYaw, setPitch, reset } = useCameraControls(context);
632
+ return /* @__PURE__ */ jsxs4(
633
+ "div",
634
+ {
635
+ style: {
636
+ display: "flex",
637
+ alignItems: "center",
638
+ gap: "6px",
639
+ background: "rgba(8,8,12,0.78)",
640
+ border: "1px solid rgba(255,255,255,0.07)",
641
+ borderRadius: "12px",
642
+ padding: "6px 8px",
643
+ backdropFilter: "blur(16px)",
644
+ boxShadow: "0 4px 24px rgba(0,0,0,0.5)",
645
+ fontFamily: '"Inter", system-ui, monospace',
646
+ ...style
647
+ },
648
+ children: [
649
+ /* @__PURE__ */ jsx4(
650
+ "div",
651
+ {
652
+ title: context ? "Camera ready" : "Waiting for context\u2026",
653
+ style: {
654
+ width: "6px",
655
+ height: "6px",
656
+ borderRadius: "50%",
657
+ background: context ? "#c3e88d" : "#555",
658
+ boxShadow: context ? "0 0 6px #c3e88daa" : "none",
659
+ flexShrink: 0,
660
+ transition: "all 0.3s"
661
+ }
662
+ }
663
+ ),
664
+ /* @__PURE__ */ jsx4(
665
+ AxisPill,
666
+ {
667
+ label: "Yaw",
668
+ color: "#ff5370",
669
+ value: camera.yaw,
670
+ onDec: () => rotate(-STEP, 0),
671
+ onInc: () => rotate(STEP, 0),
672
+ onReset: () => setYaw(0)
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx4(
676
+ AxisPill,
677
+ {
678
+ label: "Pitch",
679
+ color: "#82aaff",
680
+ value: camera.pitch,
681
+ onDec: () => rotate(0, -STEP),
682
+ onInc: () => rotate(0, STEP),
683
+ onReset: () => setPitch(0)
684
+ }
685
+ ),
686
+ /* @__PURE__ */ jsx4(IconBtn, { color: "#ffffff", title: "Reset all", onClick: reset, children: "\u2298" })
687
+ ]
688
+ }
689
+ );
690
+ }
691
+
692
+ // src/components/FaceLabel.tsx
693
+ import { jsx as jsx5 } from "react/jsx-runtime";
694
+ function transformLocalToWorld(m, x, y, z) {
695
+ return {
696
+ x: m[0] * x + m[4] * y + m[8] * z + m[12],
697
+ y: m[1] * x + m[5] * y + m[9] * z + m[13],
698
+ z: m[2] * x + m[6] * y + m[10] * z + m[14]
699
+ };
700
+ }
701
+ function cross(ax, ay, az, bx, by, bz) {
702
+ return {
703
+ x: ay * bz - az * by,
704
+ y: az * bx - ax * bz,
705
+ z: ax * by - ay * bx
706
+ };
707
+ }
708
+ function solveHomography(w, h, p) {
709
+ const [p0, p1, p2, p3] = p;
710
+ const sx = [0, w, w, 0];
711
+ const sy = [0, 0, h, h];
712
+ const dx = [p0.x, p1.x, p2.x, p3.x];
713
+ const dy = [p0.y, p1.y, p2.y, p3.y];
714
+ const M = [];
715
+ for (let i = 0; i < 4; i++) {
716
+ M.push([sx[i], sy[i], 1, 0, 0, 0, -dx[i] * sx[i], -dx[i] * sy[i], dx[i]]);
717
+ M.push([0, 0, 0, sx[i], sy[i], 1, -dy[i] * sx[i], -dy[i] * sy[i], dy[i]]);
718
+ }
719
+ const n = 8;
720
+ for (let col = 0; col < n; col++) {
721
+ let maxRow = col;
722
+ for (let row = col + 1; row < n; row++) {
723
+ if (Math.abs(M[row][col]) > Math.abs(M[maxRow][col])) maxRow = row;
724
+ }
725
+ [M[col], M[maxRow]] = [M[maxRow], M[col]];
726
+ if (Math.abs(M[col][col]) < 1e-10) return null;
727
+ const pivot = M[col][col];
728
+ for (let k = col; k <= n; k++) M[col][k] /= pivot;
729
+ for (let row = 0; row < n; row++) {
730
+ if (row === col) continue;
731
+ const f = M[row][col];
732
+ for (let k = col; k <= n; k++) M[row][k] -= f * M[col][k];
733
+ }
734
+ }
735
+ return M.map((row) => row[n]);
736
+ }
737
+ function buildMatrix3d(w, h, p) {
738
+ const hv = solveHomography(w, h, p);
739
+ if (!hv) return "none";
740
+ const [h00, h01, h02, h10, h11, h12, h20, h21] = hv;
741
+ return `matrix3d(${h00},${h10},0,${h20},${h01},${h11},0,${h21},0,0,1,0,${h02},${h12},0,1)`;
742
+ }
743
+ function FaceSkin({
744
+ context,
745
+ model,
746
+ corners,
747
+ children,
748
+ style,
749
+ className,
750
+ surfaceWidth = 300,
751
+ surfaceHeight = 200
752
+ }) {
753
+ const { project: project2 } = useWorldToScreen(context);
754
+ if (!model || !context) return null;
755
+ const mm = model.getModelMatrix();
756
+ const worldPts = corners.map(
757
+ ([lx, ly, lz]) => transformLocalToWorld(mm, lx, ly, lz)
758
+ );
759
+ const e1x = worldPts[1].x - worldPts[0].x;
760
+ const e1y = worldPts[1].y - worldPts[0].y;
761
+ const e1z = worldPts[1].z - worldPts[0].z;
762
+ const e2x = worldPts[3].x - worldPts[0].x;
763
+ const e2y = worldPts[3].y - worldPts[0].y;
764
+ const e2z = worldPts[3].z - worldPts[0].z;
765
+ const normal = cross(e1x, e1y, e1z, e2x, e2y, e2z);
766
+ const camera = context.viewport.camera;
767
+ const radius = Math.sqrt(
768
+ (camera.position[0] - camera.target[0]) ** 2 + (camera.position[1] - camera.target[1]) ** 2 + (camera.position[2] - camera.target[2]) ** 2
769
+ );
770
+ const eyeX = camera.target[0] + radius * Math.cos(camera.pitch) * Math.sin(camera.yaw);
771
+ const eyeY = camera.target[1] + radius * Math.sin(camera.pitch);
772
+ const eyeZ = camera.target[2] + radius * Math.cos(camera.pitch) * Math.cos(camera.yaw);
773
+ const toCamera = {
774
+ x: eyeX - worldPts[0].x,
775
+ y: eyeY - worldPts[0].y,
776
+ z: eyeZ - worldPts[0].z
777
+ };
778
+ const dot = normal.x * toCamera.x + normal.y * toCamera.y + normal.z * toCamera.z;
779
+ if (dot <= 0) return null;
780
+ const screenPts = worldPts.map((w) => project2(w.x, w.y, w.z));
781
+ if (screenPts.some((pt) => !pt.visible)) return null;
782
+ const [s0, s1, s2, s3] = screenPts;
783
+ const signedArea = (s1.x - s0.x) * (s2.y - s0.y) - (s2.x - s0.x) * (s1.y - s0.y);
784
+ const remapped = signedArea > 0 ? [s2, s3, s0, s1] : [s3, s2, s1, s0];
785
+ const matrix = buildMatrix3d(surfaceWidth, surfaceHeight, remapped);
786
+ return /* @__PURE__ */ jsx5(
787
+ "div",
788
+ {
789
+ style: {
790
+ position: "absolute",
791
+ inset: 0,
792
+ pointerEvents: "none",
793
+ overflow: "hidden"
794
+ },
795
+ children: /* @__PURE__ */ jsx5(
796
+ "div",
797
+ {
798
+ className,
799
+ style: {
800
+ position: "absolute",
801
+ top: 0,
802
+ left: 0,
803
+ width: surfaceWidth,
804
+ height: surfaceHeight,
805
+ transformOrigin: "0 0",
806
+ transform: matrix,
807
+ overflow: "hidden",
808
+ pointerEvents: "auto",
809
+ ...style
810
+ },
811
+ children
812
+ }
813
+ )
814
+ }
815
+ );
816
+ }
817
+
818
+ export {
819
+ GenesisGLCanvas,
820
+ ViewportGizmo,
821
+ OrbitArrows,
822
+ RotationControls,
823
+ FaceSkin
824
+ };
825
+ //# sourceMappingURL=chunk-5SN3UL2T.js.map