@layoutit/polycss-react 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,3978 @@
1
+ // src/camera/PolyPerspectiveCamera.tsx
2
+ import { memo, useMemo as useMemo2 } from "react";
3
+
4
+ // src/camera/context.ts
5
+ import { createContext, useContext } from "react";
6
+ var PolyCameraContext = createContext(null);
7
+ function useCameraContext() {
8
+ const ctx = useContext(PolyCameraContext);
9
+ if (!ctx) {
10
+ throw new Error("polycss: PolyScene must be used inside a PolyCamera.");
11
+ }
12
+ return ctx;
13
+ }
14
+
15
+ // src/camera/useCamera.ts
16
+ import { useRef as useRef2, useCallback as useCallback2, useEffect, useMemo } from "react";
17
+ import { createIsometricCamera, BASE_TILE } from "@layoutit/polycss-core";
18
+
19
+ // src/store/sceneStore.ts
20
+ import { useSyncExternalStore, useRef, useCallback } from "react";
21
+ function createSceneStore(initial) {
22
+ let state = {
23
+ cameraState: { ...initial }
24
+ };
25
+ const listeners = /* @__PURE__ */ new Set();
26
+ function notify() {
27
+ for (const listener of listeners) listener();
28
+ }
29
+ return {
30
+ getState() {
31
+ return state;
32
+ },
33
+ setState(partial) {
34
+ state = { ...state, ...partial };
35
+ notify();
36
+ },
37
+ subscribe(listener) {
38
+ listeners.add(listener);
39
+ return () => listeners.delete(listener);
40
+ },
41
+ updateCameraFromRef(handle) {
42
+ state = { cameraState: { ...handle.state } };
43
+ notify();
44
+ return true;
45
+ },
46
+ notifyAll() {
47
+ notify();
48
+ }
49
+ };
50
+ }
51
+
52
+ // src/camera/useCamera.ts
53
+ function usePolyCamera(options) {
54
+ const handleRef = useRef2(null);
55
+ if (!handleRef.current) {
56
+ handleRef.current = createIsometricCamera({
57
+ zoom: options.zoom,
58
+ target: options.target,
59
+ rotX: options.rotX,
60
+ rotY: options.rotY,
61
+ distance: options.distance
62
+ });
63
+ }
64
+ const sceneElRef = useRef2(null);
65
+ const cameraElRef = useRef2(null);
66
+ const store = useMemo(
67
+ () => createSceneStore(handleRef.current.state),
68
+ []
69
+ );
70
+ useEffect(() => {
71
+ const handle = handleRef.current;
72
+ const next = {};
73
+ if (options.zoom !== void 0) next.zoom = options.zoom;
74
+ if (options.target !== void 0) next.target = options.target;
75
+ if (options.rotX !== void 0) next.rotX = options.rotX;
76
+ if (options.rotY !== void 0) next.rotY = options.rotY;
77
+ if (options.distance !== void 0) next.distance = options.distance;
78
+ if (Object.keys(next).length > 0) {
79
+ handle.update(next);
80
+ const el = sceneElRef.current;
81
+ if (el) {
82
+ const s = handle.state;
83
+ const tileSize = BASE_TILE;
84
+ const [tx, ty, tz] = s.target;
85
+ const cssX = ty * tileSize;
86
+ const cssY = tx * tileSize;
87
+ const cssZ = tz * tileSize;
88
+ const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
89
+ el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
90
+ }
91
+ store.updateCameraFromRef(handle);
92
+ store.notifyAll();
93
+ }
94
+ }, [options.zoom, options.target, options.rotX, options.rotY, options.distance, store]);
95
+ const applyTransformDirect = useCallback2(() => {
96
+ const el = sceneElRef.current;
97
+ if (!el) return;
98
+ const handle = handleRef.current;
99
+ const s = handle.state;
100
+ const tileSize = BASE_TILE;
101
+ const [tx, ty, tz] = s.target;
102
+ const cssX = ty * tileSize;
103
+ const cssY = tx * tileSize;
104
+ const cssZ = tz * tileSize;
105
+ const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
106
+ el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
107
+ }, []);
108
+ return {
109
+ store,
110
+ cameraRef: handleRef,
111
+ sceneElRef,
112
+ cameraElRef,
113
+ applyTransformDirect
114
+ };
115
+ }
116
+
117
+ // src/camera/PolyPerspectiveCamera.tsx
118
+ import { jsx } from "react/jsx-runtime";
119
+ var DEFAULT_PERSPECTIVE = 8e3;
120
+ function PolyPerspectiveCameraInner({
121
+ zoom,
122
+ target,
123
+ rotX,
124
+ rotY,
125
+ distance,
126
+ perspective,
127
+ children,
128
+ className,
129
+ style
130
+ }) {
131
+ const {
132
+ store,
133
+ cameraRef,
134
+ sceneElRef,
135
+ cameraElRef,
136
+ applyTransformDirect
137
+ } = usePolyCamera({ zoom, target, rotX, rotY, distance });
138
+ const contextValue = useMemo2(
139
+ () => ({ store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect }),
140
+ [store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect]
141
+ );
142
+ const perspectiveValue = `${typeof perspective === "number" ? perspective : DEFAULT_PERSPECTIVE}px`;
143
+ const cameraStyle = {
144
+ ...style,
145
+ perspective: perspectiveValue
146
+ };
147
+ return /* @__PURE__ */ jsx(PolyCameraContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
148
+ "div",
149
+ {
150
+ ref: cameraElRef,
151
+ className: `polycss-camera${className ? ` ${className}` : ""}`,
152
+ style: cameraStyle,
153
+ children
154
+ }
155
+ ) });
156
+ }
157
+ var PolyPerspectiveCamera = memo(PolyPerspectiveCameraInner);
158
+
159
+ // src/camera/PolyOrthographicCamera.tsx
160
+ import { memo as memo2, useMemo as useMemo3 } from "react";
161
+ import { jsx as jsx2 } from "react/jsx-runtime";
162
+ function PolyOrthographicCameraInner({
163
+ zoom,
164
+ target,
165
+ rotX,
166
+ rotY,
167
+ distance,
168
+ children,
169
+ className,
170
+ style
171
+ }) {
172
+ const {
173
+ store,
174
+ cameraRef,
175
+ sceneElRef,
176
+ cameraElRef,
177
+ applyTransformDirect
178
+ } = usePolyCamera({ zoom, target, rotX, rotY, distance });
179
+ const contextValue = useMemo3(
180
+ () => ({ store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect }),
181
+ [store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect]
182
+ );
183
+ const cameraStyle = {
184
+ ...style,
185
+ perspective: "none"
186
+ };
187
+ return /* @__PURE__ */ jsx2(PolyCameraContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx2(
188
+ "div",
189
+ {
190
+ ref: cameraElRef,
191
+ className: `polycss-camera${className ? ` ${className}` : ""}`,
192
+ style: cameraStyle,
193
+ children
194
+ }
195
+ ) });
196
+ }
197
+ var PolyOrthographicCamera = memo2(PolyOrthographicCameraInner);
198
+
199
+ // src/scene/PolyScene.tsx
200
+ import { memo as memo3, useCallback as useCallback4, useEffect as useEffect3, useMemo as useMemo6, useRef as useRef3 } from "react";
201
+ import { createIsometricCamera as createIsometricCamera2, parseHexColor } from "@layoutit/polycss-core";
202
+
203
+ // src/scene/useSceneContext.ts
204
+ import { useMemo as useMemo4 } from "react";
205
+ import { buildSceneContext, mergePolygons } from "@layoutit/polycss-core";
206
+ function usePolySceneContext(polygons, options) {
207
+ const { directionalLight: _directionalLight } = options;
208
+ return useMemo4(() => {
209
+ const built = buildSceneContext({ polygons });
210
+ const finalPolygons = mergePolygons(built.context.polygons);
211
+ return {
212
+ polygons: finalPolygons,
213
+ sceneBbox: built.context.sceneBbox
214
+ };
215
+ }, [polygons]);
216
+ }
217
+
218
+ // src/styles/styles.ts
219
+ var POLYCSS_STYLE_ID = "polycss-styles";
220
+ function injectPolyBaseStyles(doc = typeof document !== "undefined" ? document : null) {
221
+ if (!doc || doc.getElementById(POLYCSS_STYLE_ID)) return;
222
+ const style = doc.createElement("style");
223
+ style.id = POLYCSS_STYLE_ID;
224
+ style.textContent = CORE_BASE_STYLES;
225
+ doc.head.appendChild(style);
226
+ }
227
+ var CORE_BASE_STYLES = `
228
+ /* \u2500\u2500 Scene container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
229
+
230
+ .polycss-scene,
231
+ .polycss-scene *,
232
+ .polycss-scene *::before,
233
+ .polycss-scene *::after {
234
+ box-sizing: border-box;
235
+ }
236
+
237
+ .polycss-scene {
238
+ position: absolute;
239
+ top: 50%;
240
+ left: 50%;
241
+ width: 0;
242
+ height: 0;
243
+ transform-style: preserve-3d;
244
+ perspective: none;
245
+ transform: var(--scene-transform);
246
+ }
247
+
248
+ .polycss-offset {
249
+ transform-style: preserve-3d;
250
+ transform: var(--offset-transform);
251
+ }
252
+
253
+ /* \u2500\u2500 Camera wrapper (perspective + interactive drag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
254
+
255
+ .polycss-camera {
256
+ display: flex;
257
+ width: 100%;
258
+ justify-content: center;
259
+ align-items: center;
260
+ perspective: 8000px;
261
+ min-height: inherit;
262
+ height: 100%;
263
+ position: relative;
264
+ overflow: hidden;
265
+ contain: paint;
266
+ isolation: isolate;
267
+ }
268
+ .polycss-camera * {
269
+ transform-style: preserve-3d;
270
+ position: absolute;
271
+ }
272
+
273
+ /* \u2500\u2500 Polygon leaf element \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
274
+
275
+ /*
276
+ * Polygon faces render as internal leaf elements inside .polycss-scene.
277
+ * The element is positioned absolutely within the scene root; its
278
+ * transform: matrix3d(...) carries the full world-space placement.
279
+ */
280
+ .polycss-scene b,
281
+ .polycss-scene i,
282
+ .polycss-scene s,
283
+ .polycss-scene u {
284
+ position: absolute;
285
+ display: block;
286
+ transform-origin: 0 0;
287
+ transform-style: preserve-3d;
288
+ margin: 0;
289
+ padding: 0;
290
+ font: inherit;
291
+ font-weight: normal;
292
+ font-style: normal;
293
+ line-height: 0;
294
+ quotes: none;
295
+ text-decoration: none;
296
+ backface-visibility: hidden;
297
+ background-repeat: no-repeat;
298
+ }
299
+
300
+ .polycss-scene b {
301
+ }
302
+
303
+ .polycss-scene i {
304
+ border-style: solid;
305
+ border-width: 1px;
306
+ border-color: currentColor;
307
+ }
308
+
309
+ .polycss-scene s {
310
+ }
311
+
312
+ .polycss-scene u {
313
+ width: 0px;
314
+ height: 0px;
315
+ background: transparent;
316
+ box-sizing: content-box;
317
+ border: 0 solid transparent;
318
+ }
319
+
320
+ /* \u2500\u2500 Gizmo override \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
321
+
322
+ /*
323
+ * <TransformControls> renders 3D arrows using the same polygon pipeline
324
+ * as user content, but the gizmo is a UI affordance \u2014 both faces of
325
+ * every polygon should remain visible regardless of which way the
326
+ * camera is looking. Otherwise the cuboid shafts and pyramid heads end
327
+ * up half-culled (you see only the side faces, not the caps), and the
328
+ * arrow looks like a flat strip instead of a 3D bar.
329
+ *
330
+ * Transitions on border-color and background-color smooth the
331
+ * idle \u2192 hover \u2192 drag alpha changes. Baked-mode arrows render their
332
+ * color via inline border-color, dynamic-mode via background-color
333
+ * (rgb-with-alpha CSS calc); transitioning both covers either path.
334
+ */
335
+ .polycss-transform-controls i,
336
+ .polycss-transform-controls b,
337
+ .polycss-transform-controls s,
338
+ .polycss-transform-controls u {
339
+ backface-visibility: visible;
340
+ transition: border-color 150ms ease-out, background-color 150ms ease-out;
341
+ }
342
+
343
+ /* \u2500\u2500 Dynamic lighting cascade vars (scene root \u2192 polygons) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
344
+
345
+ /*
346
+ * Dynamic mode: PolyScene writes the directional + ambient light setup to
347
+ * these custom properties on the scene root. Each polygon leaf bakes its
348
+ * own normal directly into an inline calc() that reads these vars to
349
+ * resolve the Lambert dot product and per-channel tint. Sliding the light
350
+ * only writes these scene-root vars \u2014 no JS, no atlas redraw.
351
+ *
352
+ * Registering with @property forces the browser to parse the values as
353
+ * <number>s instead of opaque token streams; that makes the polygon-level
354
+ * calc() expressions resolve reliably across engines.
355
+ */
356
+
357
+ @property --plx { syntax: "<number>"; inherits: true; initial-value: 0; }
358
+ @property --ply { syntax: "<number>"; inherits: true; initial-value: 0; }
359
+ @property --plz { syntax: "<number>"; inherits: true; initial-value: 1; }
360
+ @property --plr { syntax: "<number>"; inherits: true; initial-value: 1; }
361
+ @property --plg { syntax: "<number>"; inherits: true; initial-value: 1; }
362
+ @property --plb { syntax: "<number>"; inherits: true; initial-value: 1; }
363
+ @property --pli { syntax: "<number>"; inherits: true; initial-value: 1; }
364
+ @property --par { syntax: "<number>"; inherits: true; initial-value: 1; }
365
+ @property --pag { syntax: "<number>"; inherits: true; initial-value: 1; }
366
+ @property --pab { syntax: "<number>"; inherits: true; initial-value: 1; }
367
+ @property --pai { syntax: "<number>"; inherits: true; initial-value: 0.4; }
368
+
369
+ /* Per-polygon surface normal \u2014 set inline by the renderer. inherits:false
370
+ because each leaf has its own normal (no cascade). */
371
+ @property --pnx { syntax: "<number>"; inherits: false; initial-value: 0; }
372
+ @property --pny { syntax: "<number>"; inherits: false; initial-value: 0; }
373
+ @property --pnz { syntax: "<number>"; inherits: false; initial-value: 1; }
374
+ @property --psr { syntax: "<number>"; inherits: false; initial-value: 1; }
375
+ @property --psg { syntax: "<number>"; inherits: false; initial-value: 1; }
376
+ @property --psb { syntax: "<number>"; inherits: false; initial-value: 1; }
377
+
378
+ /* Calc-driven Lambert + tint, scoped to dynamic-lighting scenes. Lives
379
+ here (not inline per polygon) so each leaf only carries its tiny normal
380
+ declarations \u2014 ~12\xD7 smaller per-polygon style payload on big meshes. */
381
+ .polycss-scene[data-polycss-lighting="dynamic"] s {
382
+ background-color: rgb(
383
+ calc(255 * (var(--par) * var(--pai)
384
+ + var(--plr) * var(--pli) * max(0,
385
+ var(--pnx) * var(--plx) +
386
+ var(--pny) * var(--ply) +
387
+ var(--pnz) * var(--plz))))
388
+ calc(255 * (var(--pag) * var(--pai)
389
+ + var(--plg) * var(--pli) * max(0,
390
+ var(--pnx) * var(--plx) +
391
+ var(--pny) * var(--ply) +
392
+ var(--pnz) * var(--plz))))
393
+ calc(255 * (var(--pab) * var(--pai)
394
+ + var(--plb) * var(--pli) * max(0,
395
+ var(--pnx) * var(--plx) +
396
+ var(--pny) * var(--ply) +
397
+ var(--pnz) * var(--plz))))
398
+ );
399
+ background-blend-mode: multiply;
400
+ }
401
+
402
+ .polycss-scene[data-polycss-lighting="dynamic"] u {
403
+ border-bottom-color: rgb(
404
+ calc(255 * var(--psr) * (var(--par) * var(--pai)
405
+ + var(--plr) * var(--pli) * max(0,
406
+ var(--pnx) * var(--plx) +
407
+ var(--pny) * var(--ply) +
408
+ var(--pnz) * var(--plz))))
409
+ calc(255 * var(--psg) * (var(--pag) * var(--pai)
410
+ + var(--plg) * var(--pli) * max(0,
411
+ var(--pnx) * var(--plx) +
412
+ var(--pny) * var(--ply) +
413
+ var(--pnz) * var(--plz))))
414
+ calc(255 * var(--psb) * (var(--pab) * var(--pai)
415
+ + var(--plb) * var(--pli) * max(0,
416
+ var(--pnx) * var(--plx) +
417
+ var(--pny) * var(--ply) +
418
+ var(--pnz) * var(--plz))))
419
+ );
420
+ }
421
+ `;
422
+
423
+ // src/scene/textureAtlas.tsx
424
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useState } from "react";
425
+ import { parsePureColor } from "@layoutit/polycss-core";
426
+ import { jsx as jsx3 } from "react/jsx-runtime";
427
+ var DEFAULT_TILE = 50;
428
+ var DEFAULT_LIGHT_DIR = [0.4, -0.7, 0.59];
429
+ var DEFAULT_LIGHT_COLOR = "#ffffff";
430
+ var DEFAULT_LIGHT_INTENSITY = 1;
431
+ var DEFAULT_AMBIENT_COLOR = "#ffffff";
432
+ var DEFAULT_AMBIENT_INTENSITY = 0.4;
433
+ var ATLAS_MAX_SIZE = 4096;
434
+ var ATLAS_PADDING = 1;
435
+ var MIN_ATLAS_SCALE = 0.1;
436
+ var MAX_ATLAS_SCALE = 1;
437
+ var AUTO_ATLAS_LOW_AREA = ATLAS_MAX_SIZE * ATLAS_MAX_SIZE;
438
+ var AUTO_ATLAS_MEDIUM_AREA = AUTO_ATLAS_LOW_AREA * 3;
439
+ var AUTO_ATLAS_MAX_BITMAP_SIDE = 2048;
440
+ var AUTO_ATLAS_MAX_DECODED_BYTES = 16 * 1024 * 1024;
441
+ var AUTO_ATLAS_SCALE_GUARD = 0.995;
442
+ var DEFAULT_MATRIX_DECIMALS = 3;
443
+ var DEFAULT_BORDER_SHAPE_DECIMALS = 2;
444
+ var BASIS_EPS = 1e-9;
445
+ var SOLID_TRIANGLE_BLEED = 0.45;
446
+ var TEXTURE_IMAGE_CACHE = /* @__PURE__ */ new Map();
447
+ var RECT_EPS = 1e-3;
448
+ var TEXTURE_TRIANGLE_BLEED = 0.75;
449
+ function loadTextureImage(url) {
450
+ let p = TEXTURE_IMAGE_CACHE.get(url);
451
+ if (!p) {
452
+ p = new Promise((resolve, reject) => {
453
+ const img = new Image();
454
+ img.decoding = "async";
455
+ img.onload = () => resolve(img);
456
+ img.onerror = () => reject(new Error(`texture load failed: ${url}`));
457
+ img.src = url;
458
+ });
459
+ TEXTURE_IMAGE_CACHE.set(url, p);
460
+ p.then(
461
+ () => {
462
+ if (TEXTURE_IMAGE_CACHE.get(url) === p) TEXTURE_IMAGE_CACHE.delete(url);
463
+ },
464
+ () => {
465
+ if (TEXTURE_IMAGE_CACHE.get(url) === p) TEXTURE_IMAGE_CACHE.delete(url);
466
+ }
467
+ );
468
+ }
469
+ return p;
470
+ }
471
+ function normalizeAtlasScale(scale) {
472
+ const value = typeof scale === "string" ? Number(scale) : scale;
473
+ if (value === void 0 || !Number.isFinite(value)) return 1;
474
+ return Math.min(MAX_ATLAS_SCALE, Math.max(MIN_ATLAS_SCALE, value));
475
+ }
476
+ function roundDecimal(value, decimals) {
477
+ const next = value.toFixed(decimals).replace(/\.?0+$/, "");
478
+ return Object.is(Number(next), -0) ? "0" : next;
479
+ }
480
+ function formatMatrix3d(matrix, decimals = DEFAULT_MATRIX_DECIMALS) {
481
+ return `matrix3d(${matrix.split(",").map((value) => {
482
+ const parsed = Number(value.trim());
483
+ return Number.isFinite(parsed) ? roundDecimal(parsed, decimals) : value.trim();
484
+ }).join(",")})`;
485
+ }
486
+ function formatPercent(value, decimals = DEFAULT_BORDER_SHAPE_DECIMALS) {
487
+ return `${roundDecimal(value, decimals)}%`;
488
+ }
489
+ function atlasArea(pages) {
490
+ return pages.reduce((sum, page) => sum + page.width * page.height, 0);
491
+ }
492
+ function autoAtlasScaleCap(pages) {
493
+ const area = atlasArea(pages);
494
+ if (area <= 0) return 1;
495
+ const maxSide = Math.max(
496
+ 1,
497
+ ...pages.map((page) => Math.max(page.width, page.height))
498
+ );
499
+ const sideScale = AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide;
500
+ const memoryScale = Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / (area * 4));
501
+ return normalizeAtlasScale(Math.min(sideScale, memoryScale));
502
+ }
503
+ function autoAtlasScale(pages) {
504
+ const area = atlasArea(pages);
505
+ let atlasScale = 0.5;
506
+ if (area <= AUTO_ATLAS_LOW_AREA) atlasScale = 1;
507
+ else if (area <= AUTO_ATLAS_MEDIUM_AREA) atlasScale = 0.75;
508
+ return normalizeAtlasScale(Math.min(atlasScale, autoAtlasScaleCap(pages)));
509
+ }
510
+ function atlasBitmapMaxSide(pages, atlasScale) {
511
+ return pages.reduce((max, page) => Math.max(
512
+ max,
513
+ Math.ceil(page.width * atlasScale),
514
+ Math.ceil(page.height * atlasScale)
515
+ ), 0);
516
+ }
517
+ function atlasDecodedBytes(pages, atlasScale) {
518
+ return pages.reduce(
519
+ (sum, page) => sum + Math.ceil(page.width * atlasScale) * Math.ceil(page.height * atlasScale) * 4,
520
+ 0
521
+ );
522
+ }
523
+ function autoAtlasBudgetFactor(pages, atlasScale) {
524
+ const maxSide = atlasBitmapMaxSide(pages, atlasScale);
525
+ const decodedBytes = atlasDecodedBytes(pages, atlasScale);
526
+ const sideFactor = maxSide > AUTO_ATLAS_MAX_BITMAP_SIDE ? AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide : 1;
527
+ const memoryFactor = decodedBytes > AUTO_ATLAS_MAX_DECODED_BYTES ? Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / decodedBytes) : 1;
528
+ return Math.min(sideFactor, memoryFactor);
529
+ }
530
+ function packTextureAtlasPlansAuto(plans, fullScalePacked) {
531
+ let atlasScale = autoAtlasScale(fullScalePacked.pages);
532
+ let packed = atlasScale === 1 ? fullScalePacked : packTextureAtlasPlans(plans, atlasScale);
533
+ for (let i = 0; i < 4; i++) {
534
+ const factor = autoAtlasBudgetFactor(packed.pages, atlasScale);
535
+ if (factor >= 1) break;
536
+ const nextAtlasScale = normalizeAtlasScale(atlasScale * factor * AUTO_ATLAS_SCALE_GUARD);
537
+ if (nextAtlasScale >= atlasScale) break;
538
+ atlasScale = nextAtlasScale;
539
+ packed = packTextureAtlasPlans(plans, atlasScale);
540
+ }
541
+ return { packed, atlasScale };
542
+ }
543
+ function packTextureAtlasPlansWithScale(plans, atlasScaleInput) {
544
+ if (atlasScaleInput !== void 0 && atlasScaleInput !== "auto") {
545
+ const atlasScale = normalizeAtlasScale(atlasScaleInput);
546
+ return { packed: packTextureAtlasPlans(plans, atlasScale), atlasScale };
547
+ }
548
+ const fullScalePacked = packTextureAtlasPlans(plans, 1);
549
+ return packTextureAtlasPlansAuto(plans, fullScalePacked);
550
+ }
551
+ function atlasPadding(atlasScale) {
552
+ return Math.max(ATLAS_PADDING, Math.ceil(ATLAS_PADDING / atlasScale));
553
+ }
554
+ function setCssTransform(ctx, atlasScale, a = 1, b = 0, c = 0, d = 1, e = 0, f = 0) {
555
+ ctx.setTransform(
556
+ a * atlasScale,
557
+ b * atlasScale,
558
+ c * atlasScale,
559
+ d * atlasScale,
560
+ e * atlasScale,
561
+ f * atlasScale
562
+ );
563
+ }
564
+ function parseHex(hex) {
565
+ const parsed = parsePureColor(hex);
566
+ if (!parsed) return { r: 255, g: 255, b: 255 };
567
+ return { r: parsed.rgb[0], g: parsed.rgb[1], b: parsed.rgb[2] };
568
+ }
569
+ function parseAlpha(input) {
570
+ return parsePureColor(input)?.alpha ?? 1;
571
+ }
572
+ function isFullRectSolid(entry) {
573
+ if (entry.screenPts.length !== 8) return false;
574
+ const xs = [];
575
+ const ys = [];
576
+ const addUnique = (list, value) => {
577
+ for (const existing of list) {
578
+ if (Math.abs(existing - value) <= RECT_EPS) return;
579
+ }
580
+ list.push(value);
581
+ };
582
+ for (let i = 0; i < entry.screenPts.length; i += 2) {
583
+ addUnique(xs, entry.screenPts[i]);
584
+ addUnique(ys, entry.screenPts[i + 1]);
585
+ }
586
+ if (xs.length !== 2 || ys.length !== 2) return false;
587
+ xs.sort((a, b) => a - b);
588
+ ys.sort((a, b) => a - b);
589
+ if (Math.abs(xs[0]) > RECT_EPS || Math.abs(ys[0]) > RECT_EPS || xs[1] - xs[0] <= RECT_EPS || ys[1] - ys[0] <= RECT_EPS) {
590
+ return false;
591
+ }
592
+ for (let i = 0; i < entry.screenPts.length; i += 2) {
593
+ const x = entry.screenPts[i];
594
+ const y = entry.screenPts[i + 1];
595
+ const onX = Math.abs(x - xs[0]) <= RECT_EPS || Math.abs(x - xs[1]) <= RECT_EPS;
596
+ const onY = Math.abs(y - ys[0]) <= RECT_EPS || Math.abs(y - ys[1]) <= RECT_EPS;
597
+ if (!onX || !onY) return false;
598
+ }
599
+ return true;
600
+ }
601
+ function isSolidTrianglePlan(entry) {
602
+ return !entry.texture && entry.polygon.vertices.length === 3;
603
+ }
604
+ function borderShapeSupported() {
605
+ const supportsBorderShape = !!globalThis.CSS?.supports?.(
606
+ "border-shape",
607
+ "polygon(0 0, 100% 0, 0 100%) polygon(50% 50%, 50% 50%, 50% 50%)"
608
+ );
609
+ if (!supportsBorderShape) return false;
610
+ const media = globalThis.matchMedia;
611
+ if (typeof media !== "function") return true;
612
+ return media("(pointer: fine)").matches && media("(hover: hover)").matches;
613
+ }
614
+ function cssPolygonShapeForPlan(entry) {
615
+ const pts = [];
616
+ const width = entry.canvasW || 1;
617
+ const height = entry.canvasH || 1;
618
+ for (let i = 0; i < entry.screenPts.length; i += 2) {
619
+ const x = Math.max(0, Math.min(100, entry.screenPts[i] / width * 100));
620
+ const y = Math.max(0, Math.min(100, entry.screenPts[i + 1] / height * 100));
621
+ pts.push(`${formatPercent(x)} ${formatPercent(y)}`);
622
+ }
623
+ return `polygon(${pts.join(", ")})`;
624
+ }
625
+ function cssCollapsedInnerShapeForPlan(entry) {
626
+ let xSum = 0;
627
+ let ySum = 0;
628
+ const points = Math.max(1, entry.screenPts.length / 2);
629
+ for (let i = 0; i < entry.screenPts.length; i += 2) {
630
+ xSum += entry.screenPts[i];
631
+ ySum += entry.screenPts[i + 1];
632
+ }
633
+ const width = entry.canvasW || 1;
634
+ const height = entry.canvasH || 1;
635
+ const x = formatPercent(Math.max(0, Math.min(100, xSum / points / width * 100)));
636
+ const y = formatPercent(Math.max(0, Math.min(100, ySum / points / height * 100)));
637
+ return `polygon(${Array.from({ length: points }, () => `${x} ${y}`).join(", ")})`;
638
+ }
639
+ function cssBorderShapeForPlan(entry) {
640
+ return `${cssPolygonShapeForPlan(entry)} ${cssCollapsedInnerShapeForPlan(entry)}`;
641
+ }
642
+ function formatMatrix3dValues(values, decimals = DEFAULT_MATRIX_DECIMALS) {
643
+ return values.map((value) => roundDecimal(value, decimals)).join(",");
644
+ }
645
+ function cssPoints(vertices, tile, elev) {
646
+ return vertices.map((v) => [v[1] * tile, v[0] * tile, v[2] * elev]);
647
+ }
648
+ function computeSurfaceNormal(pts) {
649
+ if (pts.length < 3) return null;
650
+ const p0 = pts[0], p1 = pts[1], p2 = pts[2];
651
+ const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
652
+ const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
653
+ const normal = [
654
+ -(e1[1] * e2[2] - e1[2] * e2[1]),
655
+ -(e1[2] * e2[0] - e1[0] * e2[2]),
656
+ -(e1[0] * e2[1] - e1[1] * e2[0])
657
+ ];
658
+ const len = Math.hypot(normal[0], normal[1], normal[2]);
659
+ if (len <= BASIS_EPS) return null;
660
+ return [normal[0] / len, normal[1] / len, normal[2] / len];
661
+ }
662
+ function dotVec(a, b) {
663
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
664
+ }
665
+ function crossVec(a, b) {
666
+ return [
667
+ a[1] * b[2] - a[2] * b[1],
668
+ a[2] * b[0] - a[0] * b[2],
669
+ a[0] * b[1] - a[1] * b[0]
670
+ ];
671
+ }
672
+ function solidTriangleStyle(entry, textureLighting, pointerEvents) {
673
+ if (!isSolidTrianglePlan(entry)) return null;
674
+ const pts = cssPoints(entry.polygon.vertices, entry.tileSize, entry.layerElevation);
675
+ const normal = computeSurfaceNormal(pts);
676
+ if (!normal) return null;
677
+ const edges = [
678
+ { a: 0, b: 1, c: 2 },
679
+ { a: 1, b: 2, c: 0 },
680
+ { a: 2, b: 0, c: 1 }
681
+ ].map((edge) => {
682
+ const av2 = pts[edge.a];
683
+ const bv2 = pts[edge.b];
684
+ return {
685
+ ...edge,
686
+ length: Math.hypot(bv2[0] - av2[0], bv2[1] - av2[1], bv2[2] - av2[2])
687
+ };
688
+ }).sort((a2, b2) => b2.length - a2.length);
689
+ let a = edges[0].a;
690
+ let b = edges[0].b;
691
+ const c = edges[0].c;
692
+ let av = pts[a];
693
+ let bv = pts[b];
694
+ const cv = pts[c];
695
+ let baseLength = edges[0].length;
696
+ if (baseLength <= BASIS_EPS) return null;
697
+ let xAxis = [
698
+ (bv[0] - av[0]) / baseLength,
699
+ (bv[1] - av[1]) / baseLength,
700
+ (bv[2] - av[2]) / baseLength
701
+ ];
702
+ const ac = [cv[0] - av[0], cv[1] - av[1], cv[2] - av[2]];
703
+ let apexX = dotVec(ac, xAxis);
704
+ let foot = [
705
+ av[0] + xAxis[0] * apexX,
706
+ av[1] + xAxis[1] * apexX,
707
+ av[2] + xAxis[2] * apexX
708
+ ];
709
+ let yAxisRaw = [
710
+ foot[0] - cv[0],
711
+ foot[1] - cv[1],
712
+ foot[2] - cv[2]
713
+ ];
714
+ const height = Math.hypot(yAxisRaw[0], yAxisRaw[1], yAxisRaw[2]);
715
+ if (height <= BASIS_EPS) return null;
716
+ let yAxis = [
717
+ yAxisRaw[0] / height,
718
+ yAxisRaw[1] / height,
719
+ yAxisRaw[2] / height
720
+ ];
721
+ if (dotVec(crossVec(xAxis, yAxis), normal) < 0) {
722
+ const nextA = b;
723
+ b = a;
724
+ a = nextA;
725
+ av = pts[a];
726
+ bv = pts[b];
727
+ baseLength = Math.hypot(bv[0] - av[0], bv[1] - av[1], bv[2] - av[2]);
728
+ if (baseLength <= BASIS_EPS) return null;
729
+ xAxis = [
730
+ (bv[0] - av[0]) / baseLength,
731
+ (bv[1] - av[1]) / baseLength,
732
+ (bv[2] - av[2]) / baseLength
733
+ ];
734
+ const nextAc = [cv[0] - av[0], cv[1] - av[1], cv[2] - av[2]];
735
+ apexX = dotVec(nextAc, xAxis);
736
+ foot = [
737
+ av[0] + xAxis[0] * apexX,
738
+ av[1] + xAxis[1] * apexX,
739
+ av[2] + xAxis[2] * apexX
740
+ ];
741
+ yAxisRaw = [
742
+ foot[0] - cv[0],
743
+ foot[1] - cv[1],
744
+ foot[2] - cv[2]
745
+ ];
746
+ const nextHeight = Math.hypot(yAxisRaw[0], yAxisRaw[1], yAxisRaw[2]);
747
+ if (nextHeight <= BASIS_EPS) return null;
748
+ yAxis = [
749
+ yAxisRaw[0] / nextHeight,
750
+ yAxisRaw[1] / nextHeight,
751
+ yAxisRaw[2] / nextHeight
752
+ ];
753
+ }
754
+ const left = Math.max(0, Math.min(baseLength, apexX));
755
+ const right = Math.max(0, baseLength - left);
756
+ const bleed = SOLID_TRIANGLE_BLEED;
757
+ const leftPx = left + bleed;
758
+ const rightPx = right + bleed;
759
+ const heightPx = height + bleed * 2;
760
+ const tx = cv[0] - leftPx * xAxis[0] - bleed * yAxis[0];
761
+ const ty = cv[1] - leftPx * xAxis[1] - bleed * yAxis[1];
762
+ const tz = cv[2] - leftPx * xAxis[2] - bleed * yAxis[2];
763
+ const matrix = formatMatrix3dValues([
764
+ xAxis[0],
765
+ xAxis[1],
766
+ xAxis[2],
767
+ 0,
768
+ yAxis[0],
769
+ yAxis[1],
770
+ yAxis[2],
771
+ 0,
772
+ normal[0],
773
+ normal[1],
774
+ normal[2],
775
+ 0,
776
+ tx,
777
+ ty,
778
+ tz,
779
+ 1
780
+ ]);
781
+ const dynamic = textureLighting === "dynamic";
782
+ const base = parseHex(entry.polygon.color ?? "#cccccc");
783
+ return {
784
+ transform: `matrix3d(${matrix})`,
785
+ borderWidth: `0 ${rightPx}px ${heightPx}px ${leftPx}px`,
786
+ borderBottomColor: dynamic ? void 0 : entry.shadedColor,
787
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
788
+ ...dynamic ? {
789
+ ["--pnx"]: normal[0].toFixed(4),
790
+ ["--pny"]: normal[1].toFixed(4),
791
+ ["--pnz"]: normal[2].toFixed(4),
792
+ ["--psr"]: (base.r / 255).toFixed(4),
793
+ ["--psg"]: (base.g / 255).toFixed(4),
794
+ ["--psb"]: (base.b / 255).toFixed(4)
795
+ } : null
796
+ };
797
+ }
798
+ function rgbToHex({ r, g, b }) {
799
+ const f = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
800
+ return `#${f(r)}${f(g)}${f(b)}`;
801
+ }
802
+ function shadePolygon(baseColor, directScale, lightColor, ambientColor, ambientIntensity) {
803
+ const base = parseHex(baseColor);
804
+ const light = parseHex(lightColor);
805
+ const amb = parseHex(ambientColor);
806
+ const tintR = amb.r / 255 * ambientIntensity + light.r / 255 * directScale;
807
+ const tintG = amb.g / 255 * ambientIntensity + light.g / 255 * directScale;
808
+ const tintB = amb.b / 255 * ambientIntensity + light.b / 255 * directScale;
809
+ const r = Math.max(0, Math.min(255, Math.round(base.r * tintR)));
810
+ const g = Math.max(0, Math.min(255, Math.round(base.g * tintG)));
811
+ const b = Math.max(0, Math.min(255, Math.round(base.b * tintB)));
812
+ const alpha = parseAlpha(baseColor);
813
+ return alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${alpha})` : rgbToHex({ r, g, b });
814
+ }
815
+ function textureTintFactors(directScale, lightColor, ambientColor, ambientIntensity) {
816
+ const light = parseHex(lightColor);
817
+ const amb = parseHex(ambientColor);
818
+ return {
819
+ r: amb.r / 255 * ambientIntensity + light.r / 255 * directScale,
820
+ g: amb.g / 255 * ambientIntensity + light.g / 255 * directScale,
821
+ b: amb.b / 255 * ambientIntensity + light.b / 255 * directScale
822
+ };
823
+ }
824
+ function tintToCss({ r, g, b }) {
825
+ const f = (n) => Math.round(Math.max(0, Math.min(1, n)) * 255);
826
+ return `rgb(${f(r)} ${f(g)} ${f(b)})`;
827
+ }
828
+ function applyTextureTint(ctx, x, y, width, height, tint, atlasScale) {
829
+ if (Math.abs(tint.r - 1) < 1e-3 && Math.abs(tint.g - 1) < 1e-3 && Math.abs(tint.b - 1) < 1e-3) {
830
+ return;
831
+ }
832
+ ctx.save();
833
+ setCssTransform(ctx, atlasScale);
834
+ ctx.globalCompositeOperation = "multiply";
835
+ ctx.fillStyle = tintToCss(tint);
836
+ ctx.fillRect(x, y, width, height);
837
+ ctx.restore();
838
+ }
839
+ function drawImageCover(ctx, img, x, y, width, height, atlasScale) {
840
+ const srcW = img.naturalWidth || img.width || 1;
841
+ const srcH = img.naturalHeight || img.height || 1;
842
+ const scale = Math.max(width / srcW, height / srcH);
843
+ const drawW = srcW * scale;
844
+ const drawH = srcH * scale;
845
+ setCssTransform(ctx, atlasScale);
846
+ ctx.drawImage(img, x + (width - drawW) / 2, y + (height - drawH) / 2, drawW, drawH);
847
+ }
848
+ function computeUvAffine(points, uvs) {
849
+ if (points.length < 3 || uvs.length < 3) return null;
850
+ const [p0, p1, p2] = points;
851
+ const [uv0, uv1, uv2] = uvs;
852
+ const sx0 = p0[0], sy0 = p0[1];
853
+ const sx1 = p1[0], sy1 = p1[1];
854
+ const sx2 = p2[0], sy2 = p2[1];
855
+ const u0 = uv0[0], V0 = 1 - uv0[1];
856
+ const u1 = uv1[0], V1 = 1 - uv1[1];
857
+ const u2 = uv2[0], V2 = 1 - uv2[1];
858
+ const du1 = u1 - u0, dV1 = V1 - V0;
859
+ const du2 = u2 - u0, dV2 = V2 - V0;
860
+ const det = du1 * dV2 - du2 * dV1;
861
+ if (Math.abs(det) <= 1e-9) return null;
862
+ const dx1 = sx1 - sx0, dx2 = sx2 - sx0;
863
+ const dy1 = sy1 - sy0, dy2 = sy2 - sy0;
864
+ const affine = {
865
+ a: (dx1 * dV2 - dx2 * dV1) / det,
866
+ b: (du1 * dx2 - du2 * dx1) / det,
867
+ c: (dy1 * dV2 - dy2 * dV1) / det,
868
+ d: (du1 * dy2 - du2 * dy1) / det,
869
+ e: 0,
870
+ f: 0
871
+ };
872
+ affine.e = sx0 - affine.a * u0 - affine.b * V0;
873
+ affine.f = sy0 - affine.c * u0 - affine.d * V0;
874
+ return affine;
875
+ }
876
+ function computeUvSampleRect(uvs) {
877
+ if (uvs.length === 0) return null;
878
+ let minU = Infinity;
879
+ let minV = Infinity;
880
+ let maxU = -Infinity;
881
+ let maxV = -Infinity;
882
+ for (const uv of uvs) {
883
+ const u = uv[0];
884
+ const v = 1 - uv[1];
885
+ if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
886
+ minU = Math.min(minU, u);
887
+ maxU = Math.max(maxU, u);
888
+ minV = Math.min(minV, v);
889
+ maxV = Math.max(maxV, v);
890
+ }
891
+ return { minU, minV, maxU, maxV };
892
+ }
893
+ function projectTextureTriangle(triangle, tile, elev, origin, xAxis, yAxis, shiftX, shiftY) {
894
+ const points = triangle.vertices.map((vertex) => {
895
+ const point = [
896
+ vertex[1] * tile,
897
+ vertex[0] * tile,
898
+ vertex[2] * elev
899
+ ];
900
+ const dx = point[0] - origin[0];
901
+ const dy = point[1] - origin[1];
902
+ const dz = point[2] - origin[2];
903
+ return [
904
+ dx * xAxis[0] + dy * xAxis[1] + dz * xAxis[2] + shiftX,
905
+ dx * yAxis[0] + dy * yAxis[1] + dz * yAxis[2] + shiftY
906
+ ];
907
+ });
908
+ const uvAffine = computeUvAffine(points, triangle.uvs);
909
+ const uvSampleRect = computeUvSampleRect(triangle.uvs);
910
+ if (!uvAffine && !uvSampleRect) return null;
911
+ return {
912
+ screenPts: points.flatMap(([x, y]) => [x, y]),
913
+ uvAffine,
914
+ uvSampleRect
915
+ };
916
+ }
917
+ function expandClipPoints(points, amount) {
918
+ if (points.length < 6 || amount <= 0) return points;
919
+ let cx = 0;
920
+ let cy = 0;
921
+ const count = points.length / 2;
922
+ for (let i = 0; i < points.length; i += 2) {
923
+ cx += points[i];
924
+ cy += points[i + 1];
925
+ }
926
+ cx /= count;
927
+ cy /= count;
928
+ const expanded = points.slice();
929
+ for (let i = 0; i < expanded.length; i += 2) {
930
+ const dx = expanded[i] - cx;
931
+ const dy = expanded[i + 1] - cy;
932
+ const len = Math.hypot(dx, dy);
933
+ if (len <= RECT_EPS) continue;
934
+ expanded[i] += dx / len * amount;
935
+ expanded[i + 1] += dy / len * amount;
936
+ }
937
+ return expanded;
938
+ }
939
+ function canvasToUrl(canvas) {
940
+ if (typeof canvas.toBlob === "function") {
941
+ return new Promise((resolve) => {
942
+ canvas.toBlob((blob) => {
943
+ resolve(blob ? URL.createObjectURL(blob) : null);
944
+ }, "image/png");
945
+ });
946
+ }
947
+ try {
948
+ return Promise.resolve(canvas.toDataURL("image/png"));
949
+ } catch {
950
+ return Promise.resolve(null);
951
+ }
952
+ }
953
+ function clampSourceCoord(value, max) {
954
+ return Math.max(0, Math.min(max, value));
955
+ }
956
+ function drawImageUvSample(ctx, img, rect, x, y, width, height, atlasScale) {
957
+ const imgW = img.naturalWidth || img.width || 1;
958
+ const imgH = img.naturalHeight || img.height || 1;
959
+ const rawX0 = clampSourceCoord(Math.min(rect.minU, rect.maxU) * imgW, imgW);
960
+ const rawX1 = clampSourceCoord(Math.max(rect.minU, rect.maxU) * imgW, imgW);
961
+ const rawY0 = clampSourceCoord(Math.min(rect.minV, rect.maxV) * imgH, imgH);
962
+ const rawY1 = clampSourceCoord(Math.max(rect.minV, rect.maxV) * imgH, imgH);
963
+ let sx = Math.floor(rawX0);
964
+ let sy = Math.floor(rawY0);
965
+ let sw = Math.ceil(rawX1) - sx;
966
+ let sh = Math.ceil(rawY1) - sy;
967
+ if (sw < 1) {
968
+ sx = Math.floor(clampSourceCoord((rect.minU + rect.maxU) / 2 * imgW, imgW - 1));
969
+ sw = 1;
970
+ }
971
+ if (sh < 1) {
972
+ sy = Math.floor(clampSourceCoord((rect.minV + rect.maxV) / 2 * imgH, imgH - 1));
973
+ sh = 1;
974
+ }
975
+ sx = Math.max(0, Math.min(imgW - 1, sx));
976
+ sy = Math.max(0, Math.min(imgH - 1, sy));
977
+ sw = Math.max(1, Math.min(imgW - sx, sw));
978
+ sh = Math.max(1, Math.min(imgH - sy, sh));
979
+ setCssTransform(ctx, atlasScale);
980
+ ctx.drawImage(img, sx, sy, sw, sh, x, y, width, height);
981
+ }
982
+ function computeTextureAtlasPlan(polygon, index, options = {}) {
983
+ const { vertices, texture, uvs } = polygon;
984
+ if (!vertices || vertices.length < 3) return null;
985
+ const tile = options.tileSize ?? DEFAULT_TILE;
986
+ const elev = options.layerElevation ?? tile;
987
+ const toCss = (v) => [
988
+ v[1] * tile,
989
+ v[0] * tile,
990
+ v[2] * elev
991
+ ];
992
+ const pts = vertices.map(toCss);
993
+ const p0 = pts[0];
994
+ const p1 = pts[1];
995
+ const p2 = pts[2];
996
+ const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
997
+ const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
998
+ const l01 = Math.hypot(e1[0], e1[1], e1[2]);
999
+ if (l01 === 0) return null;
1000
+ const xAxis = [e1[0] / l01, e1[1] / l01, e1[2] / l01];
1001
+ let nx = -(e1[1] * e2[2] - e1[2] * e2[1]);
1002
+ let ny = -(e1[2] * e2[0] - e1[0] * e2[2]);
1003
+ let nz = -(e1[0] * e2[1] - e1[1] * e2[0]);
1004
+ const nLen = Math.hypot(nx, ny, nz);
1005
+ if (nLen === 0) return null;
1006
+ nx /= nLen;
1007
+ ny /= nLen;
1008
+ nz /= nLen;
1009
+ const yAxis = [
1010
+ ny * xAxis[2] - nz * xAxis[1],
1011
+ nz * xAxis[0] - nx * xAxis[2],
1012
+ nx * xAxis[1] - ny * xAxis[0]
1013
+ ];
1014
+ const local2D = pts.map((p) => {
1015
+ const dx = p[0] - p0[0], dy = p[1] - p0[1], dz = p[2] - p0[2];
1016
+ return [
1017
+ dx * xAxis[0] + dy * xAxis[1] + dz * xAxis[2],
1018
+ dx * yAxis[0] + dy * yAxis[1] + dz * yAxis[2]
1019
+ ];
1020
+ });
1021
+ let xMin = Infinity, yMin = Infinity, xMax = -Infinity, yMax = -Infinity;
1022
+ for (const [x, y] of local2D) {
1023
+ if (x < xMin) xMin = x;
1024
+ if (x > xMax) xMax = x;
1025
+ if (y < yMin) yMin = y;
1026
+ if (y > yMax) yMax = y;
1027
+ }
1028
+ const shiftX = -xMin;
1029
+ const shiftY = -yMin;
1030
+ const w = xMax - xMin;
1031
+ const h = yMax - yMin;
1032
+ if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
1033
+ const screenPts = [];
1034
+ for (let i = 0; i < local2D.length; i++) {
1035
+ const [x, y] = local2D[i];
1036
+ const sx = x + shiftX;
1037
+ const sy = y + shiftY;
1038
+ screenPts.push(sx, sy);
1039
+ }
1040
+ const tx = p0[0] - shiftX * xAxis[0] - shiftY * yAxis[0];
1041
+ const ty = p0[1] - shiftX * xAxis[1] - shiftY * yAxis[1];
1042
+ const tz = p0[2] - shiftX * xAxis[2] - shiftY * yAxis[2];
1043
+ const matrix = [
1044
+ xAxis[0],
1045
+ xAxis[1],
1046
+ xAxis[2],
1047
+ 0,
1048
+ yAxis[0],
1049
+ yAxis[1],
1050
+ yAxis[2],
1051
+ 0,
1052
+ nx,
1053
+ ny,
1054
+ nz,
1055
+ 0,
1056
+ tx,
1057
+ ty,
1058
+ tz,
1059
+ 1
1060
+ ].join(",");
1061
+ const directionalCfg = options.directionalLight;
1062
+ const ambientCfg = options.ambientLight;
1063
+ const lightDir = directionalCfg?.direction ?? DEFAULT_LIGHT_DIR;
1064
+ const lightColor = directionalCfg?.color ?? DEFAULT_LIGHT_COLOR;
1065
+ const lightIntensity = Math.max(0, directionalCfg?.intensity ?? DEFAULT_LIGHT_INTENSITY);
1066
+ const ambientColor = ambientCfg?.color ?? DEFAULT_AMBIENT_COLOR;
1067
+ const ambientIntensity = Math.max(0, ambientCfg?.intensity ?? DEFAULT_AMBIENT_INTENSITY);
1068
+ const lLen = Math.hypot(lightDir[0], lightDir[1], lightDir[2]) || 1;
1069
+ const lx = lightDir[0] / lLen, ly = lightDir[1] / lLen, lz = lightDir[2] / lLen;
1070
+ const directScale = lightIntensity * Math.max(0, nx * lx + ny * ly + nz * lz);
1071
+ const textureTint = textureTintFactors(directScale, lightColor, ambientColor, ambientIntensity);
1072
+ const shadedColor = shadePolygon(polygon.color ?? "#cccccc", directScale, lightColor, ambientColor, ambientIntensity);
1073
+ let uvAffine = null;
1074
+ let uvSampleRect = null;
1075
+ if (texture && uvs && uvs.length >= 3 && uvs.length === vertices.length) {
1076
+ uvSampleRect = computeUvSampleRect(uvs);
1077
+ uvAffine = computeUvAffine(
1078
+ local2D.map(([x, y]) => [x + shiftX, y + shiftY]),
1079
+ uvs
1080
+ );
1081
+ }
1082
+ const textureTriangles = texture && polygon.textureTriangles?.length ? polygon.textureTriangles.map(
1083
+ (triangle) => projectTextureTriangle(triangle, tile, elev, p0, xAxis, yAxis, shiftX, shiftY)
1084
+ ).filter((triangle) => !!triangle) : null;
1085
+ return {
1086
+ index,
1087
+ polygon,
1088
+ texture,
1089
+ tileSize: tile,
1090
+ layerElevation: elev,
1091
+ matrix,
1092
+ canvasW: Math.max(1, Math.ceil(w)),
1093
+ canvasH: Math.max(1, Math.ceil(h)),
1094
+ screenPts,
1095
+ uvAffine,
1096
+ uvSampleRect,
1097
+ textureTriangles,
1098
+ normal: [nx, ny, nz],
1099
+ textureTint,
1100
+ shadedColor
1101
+ };
1102
+ }
1103
+ function packTextureAtlasPlans(plans, atlasScale = 1) {
1104
+ const entries = Array(plans.length).fill(null);
1105
+ const pages = [];
1106
+ const padding = atlasPadding(atlasScale);
1107
+ const sortedPlans = plans.filter((plan) => !!plan).sort(
1108
+ (a, b) => b.canvasH - a.canvasH || b.canvasW - a.canvasW || a.index - b.index
1109
+ );
1110
+ const createPage = () => ({
1111
+ width: padding,
1112
+ height: padding,
1113
+ entries: [],
1114
+ shelves: []
1115
+ });
1116
+ const placeOnPage = (page, plan, pageIndex) => {
1117
+ if (page.sealed) return null;
1118
+ for (const shelf of page.shelves) {
1119
+ if (plan.canvasH <= shelf.height && shelf.x + plan.canvasW + padding <= ATLAS_MAX_SIZE) {
1120
+ const entry2 = { ...plan, pageIndex, x: shelf.x, y: shelf.y };
1121
+ shelf.x += plan.canvasW + padding;
1122
+ page.entries.push(entry2);
1123
+ page.width = Math.max(page.width, entry2.x + plan.canvasW + padding);
1124
+ return entry2;
1125
+ }
1126
+ }
1127
+ const shelfY = page.shelves.length === 0 ? padding : page.height;
1128
+ if (shelfY + plan.canvasH + padding > ATLAS_MAX_SIZE) return null;
1129
+ const entry = { ...plan, pageIndex, x: padding, y: shelfY };
1130
+ page.shelves.push({
1131
+ x: padding + plan.canvasW + padding,
1132
+ y: shelfY,
1133
+ height: plan.canvasH
1134
+ });
1135
+ page.entries.push(entry);
1136
+ page.width = Math.max(page.width, entry.x + plan.canvasW + padding);
1137
+ page.height = Math.max(page.height, shelfY + plan.canvasH + padding);
1138
+ return entry;
1139
+ };
1140
+ for (const plan of sortedPlans) {
1141
+ const tooLarge = plan.canvasW + padding * 2 > ATLAS_MAX_SIZE || plan.canvasH + padding * 2 > ATLAS_MAX_SIZE;
1142
+ if (tooLarge) {
1143
+ const pageIndex = pages.length;
1144
+ const entry = {
1145
+ ...plan,
1146
+ pageIndex,
1147
+ x: padding,
1148
+ y: padding
1149
+ };
1150
+ entries[plan.index] = entry;
1151
+ pages.push({
1152
+ width: plan.canvasW + padding * 2,
1153
+ height: plan.canvasH + padding * 2,
1154
+ entries: [entry],
1155
+ shelves: [],
1156
+ sealed: true
1157
+ });
1158
+ continue;
1159
+ }
1160
+ let placed = null;
1161
+ for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
1162
+ placed = placeOnPage(pages[pageIndex], plan, pageIndex);
1163
+ if (placed) break;
1164
+ }
1165
+ if (!placed) {
1166
+ const page = createPage();
1167
+ const pageIndex = pages.length;
1168
+ pages.push(page);
1169
+ placed = placeOnPage(page, plan, pageIndex);
1170
+ }
1171
+ if (placed) entries[plan.index] = placed;
1172
+ }
1173
+ return {
1174
+ entries,
1175
+ pages: pages.map(({ width, height, entries: entries2 }) => ({ width, height, entries: entries2 }))
1176
+ };
1177
+ }
1178
+ async function buildAtlasPage(page, textureLighting, doc, atlasScale) {
1179
+ const canvas = doc.createElement("canvas");
1180
+ canvas.width = Math.max(1, Math.ceil(page.width * atlasScale));
1181
+ canvas.height = Math.max(1, Math.ceil(page.height * atlasScale));
1182
+ const ctx = canvas.getContext("2d");
1183
+ if (!ctx) return { width: page.width, height: page.height, url: null };
1184
+ const uniqueTextures = Array.from(new Set(
1185
+ page.entries.flatMap((entry) => entry.texture ? [entry.texture] : [])
1186
+ ));
1187
+ const loaded = /* @__PURE__ */ new Map();
1188
+ await Promise.all(uniqueTextures.map(async (url2) => {
1189
+ loaded.set(url2, await loadTextureImage(url2));
1190
+ }));
1191
+ for (const entry of page.entries) {
1192
+ const srcImg = entry.texture ? loaded.get(entry.texture) : null;
1193
+ ctx.save();
1194
+ setCssTransform(ctx, atlasScale);
1195
+ ctx.beginPath();
1196
+ for (let i = 0; i < entry.screenPts.length; i += 2) {
1197
+ const px = entry.x + entry.screenPts[i];
1198
+ const py = entry.y + entry.screenPts[i + 1];
1199
+ if (i === 0) ctx.moveTo(px, py);
1200
+ else ctx.lineTo(px, py);
1201
+ }
1202
+ ctx.closePath();
1203
+ ctx.clip();
1204
+ if (!entry.texture) {
1205
+ setCssTransform(ctx, atlasScale);
1206
+ ctx.fillStyle = textureLighting === "dynamic" ? entry.polygon.color ?? "#cccccc" : entry.shadedColor;
1207
+ ctx.fillRect(entry.x, entry.y, entry.canvasW, entry.canvasH);
1208
+ } else {
1209
+ if (srcImg && entry.textureTriangles?.length) {
1210
+ const imgW = srcImg.naturalWidth || srcImg.width || 1;
1211
+ const imgH = srcImg.naturalHeight || srcImg.height || 1;
1212
+ for (const triangle of entry.textureTriangles) {
1213
+ const clipPts = expandClipPoints(triangle.screenPts, TEXTURE_TRIANGLE_BLEED);
1214
+ ctx.save();
1215
+ setCssTransform(ctx, atlasScale);
1216
+ ctx.beginPath();
1217
+ for (let i = 0; i < clipPts.length; i += 2) {
1218
+ const px = entry.x + clipPts[i];
1219
+ const py = entry.y + clipPts[i + 1];
1220
+ if (i === 0) ctx.moveTo(px, py);
1221
+ else ctx.lineTo(px, py);
1222
+ }
1223
+ ctx.closePath();
1224
+ ctx.clip();
1225
+ if (triangle.uvAffine) {
1226
+ setCssTransform(
1227
+ ctx,
1228
+ atlasScale,
1229
+ triangle.uvAffine.a / imgW,
1230
+ triangle.uvAffine.c / imgW,
1231
+ triangle.uvAffine.b / imgH,
1232
+ triangle.uvAffine.d / imgH,
1233
+ entry.x + triangle.uvAffine.e,
1234
+ entry.y + triangle.uvAffine.f
1235
+ );
1236
+ ctx.drawImage(srcImg, 0, 0);
1237
+ } else if (triangle.uvSampleRect) {
1238
+ drawImageUvSample(
1239
+ ctx,
1240
+ srcImg,
1241
+ triangle.uvSampleRect,
1242
+ entry.x,
1243
+ entry.y,
1244
+ entry.canvasW,
1245
+ entry.canvasH,
1246
+ atlasScale
1247
+ );
1248
+ }
1249
+ ctx.restore();
1250
+ }
1251
+ } else if (srcImg && entry.uvAffine) {
1252
+ const imgW = srcImg.naturalWidth || srcImg.width || 1;
1253
+ const imgH = srcImg.naturalHeight || srcImg.height || 1;
1254
+ setCssTransform(
1255
+ ctx,
1256
+ atlasScale,
1257
+ entry.uvAffine.a / imgW,
1258
+ entry.uvAffine.c / imgW,
1259
+ entry.uvAffine.b / imgH,
1260
+ entry.uvAffine.d / imgH,
1261
+ entry.x + entry.uvAffine.e,
1262
+ entry.y + entry.uvAffine.f
1263
+ );
1264
+ ctx.drawImage(srcImg, 0, 0);
1265
+ } else if (srcImg && entry.uvSampleRect) {
1266
+ drawImageUvSample(
1267
+ ctx,
1268
+ srcImg,
1269
+ entry.uvSampleRect,
1270
+ entry.x,
1271
+ entry.y,
1272
+ entry.canvasW,
1273
+ entry.canvasH,
1274
+ atlasScale
1275
+ );
1276
+ } else if (srcImg) {
1277
+ drawImageCover(ctx, srcImg, entry.x, entry.y, entry.canvasW, entry.canvasH, atlasScale);
1278
+ }
1279
+ }
1280
+ if (entry.texture && textureLighting === "baked") {
1281
+ applyTextureTint(ctx, entry.x, entry.y, entry.canvasW, entry.canvasH, entry.textureTint, atlasScale);
1282
+ }
1283
+ ctx.restore();
1284
+ }
1285
+ const url = await canvasToUrl(canvas);
1286
+ canvas.width = 1;
1287
+ canvas.height = 1;
1288
+ return {
1289
+ width: page.width,
1290
+ height: page.height,
1291
+ url
1292
+ };
1293
+ }
1294
+ async function buildAtlasPages(pages, textureLighting, doc, atlasScale, isCancelled) {
1295
+ const built = [];
1296
+ for (const page of pages) {
1297
+ if (isCancelled()) break;
1298
+ built.push(await buildAtlasPage(page, textureLighting, doc, atlasScale));
1299
+ }
1300
+ return built;
1301
+ }
1302
+ function useTextureAtlas(plans, textureLighting, atlasScaleInput) {
1303
+ const useBorderShape = textureLighting !== "dynamic" && borderShapeSupported();
1304
+ const atlasPlans = useMemo5(
1305
+ () => plans.map((plan) => {
1306
+ if (!plan) return plan;
1307
+ if (plan.texture) return plan;
1308
+ if (isSolidTrianglePlan(plan)) return null;
1309
+ if (textureLighting !== "dynamic" && (useBorderShape || isFullRectSolid(plan))) return null;
1310
+ return plan;
1311
+ }),
1312
+ [plans, textureLighting, useBorderShape]
1313
+ );
1314
+ const { packed, atlasScale } = useMemo5(
1315
+ () => packTextureAtlasPlansWithScale(atlasPlans, atlasScaleInput),
1316
+ [atlasPlans, atlasScaleInput]
1317
+ );
1318
+ const [pages, setPages] = useState(
1319
+ () => packed.pages.map((page) => ({ width: page.width, height: page.height, url: null }))
1320
+ );
1321
+ useEffect2(() => {
1322
+ let cancelled = false;
1323
+ let urls = [];
1324
+ setPages(packed.pages.map((page) => ({ width: page.width, height: page.height, url: null })));
1325
+ if (packed.pages.length === 0 || typeof document === "undefined") {
1326
+ return () => {
1327
+ };
1328
+ }
1329
+ buildAtlasPages(packed.pages, textureLighting, document, atlasScale, () => cancelled).then((nextPages) => {
1330
+ if (cancelled) {
1331
+ for (const page of nextPages) {
1332
+ if (page.url?.startsWith("blob:")) URL.revokeObjectURL(page.url);
1333
+ }
1334
+ return;
1335
+ }
1336
+ urls = nextPages.flatMap((page) => page.url?.startsWith("blob:") ? [page.url] : []);
1337
+ setPages(nextPages);
1338
+ }).catch(() => {
1339
+ if (!cancelled) {
1340
+ setPages(packed.pages.map((page) => ({ width: page.width, height: page.height, url: null })));
1341
+ }
1342
+ });
1343
+ return () => {
1344
+ cancelled = true;
1345
+ for (const url of urls) URL.revokeObjectURL(url);
1346
+ };
1347
+ }, [packed, textureLighting, atlasScale]);
1348
+ return {
1349
+ entries: packed.entries,
1350
+ pages,
1351
+ ready: pages.length === 0 || pages.every((page) => !!page.url)
1352
+ };
1353
+ }
1354
+ function TextureBorderShapePoly({
1355
+ entry,
1356
+ className,
1357
+ style: styleProp,
1358
+ domAttrs,
1359
+ domEventHandlers,
1360
+ pointerEvents = "auto"
1361
+ }) {
1362
+ const fullRect = isFullRectSolid(entry);
1363
+ const borderShape = fullRect ? null : cssBorderShapeForPlan(entry);
1364
+ const setElementRef = useCallback3((el) => {
1365
+ if (!el) return;
1366
+ if (borderShape) el.style.setProperty("border-shape", borderShape);
1367
+ else el.style.removeProperty("border-shape");
1368
+ }, [borderShape]);
1369
+ const style = fullRect ? {
1370
+ width: entry.canvasW,
1371
+ height: entry.canvasH,
1372
+ transform: formatMatrix3d(entry.matrix),
1373
+ background: entry.shadedColor,
1374
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
1375
+ ...styleProp
1376
+ } : {
1377
+ width: entry.canvasW,
1378
+ height: entry.canvasH,
1379
+ transform: formatMatrix3d(entry.matrix),
1380
+ color: entry.shadedColor,
1381
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
1382
+ ...styleProp
1383
+ };
1384
+ const dataAttrs = entry.polygon.data ? Object.fromEntries(
1385
+ Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
1386
+ ) : {};
1387
+ const elementClassName = className?.trim() || void 0;
1388
+ if (fullRect) {
1389
+ return /* @__PURE__ */ jsx3(
1390
+ "b",
1391
+ {
1392
+ className: elementClassName,
1393
+ style,
1394
+ ...domEventHandlers,
1395
+ ...dataAttrs,
1396
+ ...domAttrs
1397
+ }
1398
+ );
1399
+ }
1400
+ return /* @__PURE__ */ jsx3(
1401
+ "i",
1402
+ {
1403
+ ref: setElementRef,
1404
+ className: elementClassName,
1405
+ style,
1406
+ ...domEventHandlers,
1407
+ ...dataAttrs,
1408
+ ...domAttrs
1409
+ }
1410
+ );
1411
+ }
1412
+ function TextureTrianglePoly({
1413
+ entry,
1414
+ textureLighting,
1415
+ className,
1416
+ style: styleProp,
1417
+ domAttrs,
1418
+ domEventHandlers,
1419
+ pointerEvents = "auto"
1420
+ }) {
1421
+ const triangleStyle = solidTriangleStyle(entry, textureLighting, pointerEvents);
1422
+ if (!triangleStyle) return null;
1423
+ const dataAttrs = entry.polygon.data ? Object.fromEntries(
1424
+ Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
1425
+ ) : {};
1426
+ const elementClassName = className?.trim() || void 0;
1427
+ return /* @__PURE__ */ jsx3(
1428
+ "u",
1429
+ {
1430
+ className: elementClassName,
1431
+ style: { ...triangleStyle, ...styleProp },
1432
+ ...domEventHandlers,
1433
+ ...dataAttrs,
1434
+ ...domAttrs
1435
+ }
1436
+ );
1437
+ }
1438
+ function TextureAtlasPoly({
1439
+ entry,
1440
+ page,
1441
+ textureLighting,
1442
+ className,
1443
+ style: styleProp,
1444
+ domAttrs,
1445
+ domEventHandlers,
1446
+ pointerEvents = "auto"
1447
+ }) {
1448
+ const dynamic = textureLighting === "dynamic";
1449
+ const dynamicMask = dynamic && page?.url ? `url(${page.url})` : void 0;
1450
+ const background = !dynamic && page?.url ? `url(${page.url}) -${entry.x}px -${entry.y}px / ${page.width}px ${page.height}px no-repeat` : void 0;
1451
+ const style = {
1452
+ width: entry.canvasW,
1453
+ height: entry.canvasH,
1454
+ transform: formatMatrix3d(entry.matrix),
1455
+ background,
1456
+ backgroundImage: dynamic && page?.url ? `url(${page.url})` : void 0,
1457
+ backgroundPosition: dynamic ? `-${entry.x}px -${entry.y}px` : void 0,
1458
+ backgroundSize: dynamic && page ? `${page.width}px ${page.height}px` : void 0,
1459
+ ...dynamic ? {
1460
+ ["--pnx"]: entry.normal[0].toFixed(4),
1461
+ ["--pny"]: entry.normal[1].toFixed(4),
1462
+ ["--pnz"]: entry.normal[2].toFixed(4)
1463
+ } : null,
1464
+ ...dynamic && dynamicMask ? {
1465
+ // Use the atlas as an alpha mask so transparent regions outside
1466
+ // the polygon don't get painted with the tint.
1467
+ maskImage: dynamicMask,
1468
+ maskMode: "alpha",
1469
+ maskPosition: `-${entry.x}px -${entry.y}px`,
1470
+ maskSize: page ? `${page.width}px ${page.height}px` : void 0,
1471
+ maskRepeat: "no-repeat",
1472
+ WebkitMaskImage: dynamicMask,
1473
+ WebkitMaskPosition: `-${entry.x}px -${entry.y}px`,
1474
+ WebkitMaskSize: page ? `${page.width}px ${page.height}px` : void 0,
1475
+ WebkitMaskRepeat: "no-repeat"
1476
+ } : null,
1477
+ opacity: page?.url ? void 0 : 0,
1478
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
1479
+ ...styleProp
1480
+ };
1481
+ const dataAttrs = entry.polygon.data ? Object.fromEntries(
1482
+ Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
1483
+ ) : {};
1484
+ const elementClassName = className?.trim() || void 0;
1485
+ return /* @__PURE__ */ jsx3(
1486
+ "s",
1487
+ {
1488
+ className: elementClassName,
1489
+ style,
1490
+ ...domEventHandlers,
1491
+ ...dataAttrs,
1492
+ ...domAttrs
1493
+ }
1494
+ );
1495
+ }
1496
+
1497
+ // src/scene/sceneContext.ts
1498
+ import { createContext as createContext2, useContext as useContext2 } from "react";
1499
+ var PolySceneContext = createContext2(null);
1500
+ function usePolySceneContext2() {
1501
+ return useContext2(PolySceneContext);
1502
+ }
1503
+
1504
+ // src/scene/PolyScene.tsx
1505
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
1506
+ function PolySceneInner({
1507
+ polygons: polygonsProp,
1508
+ centerPolygons: centerPolygonsProp,
1509
+ perspective: _perspective,
1510
+ rotX: _rotX,
1511
+ rotY: _rotY,
1512
+ zoom: _zoom,
1513
+ directionalLight,
1514
+ ambientLight,
1515
+ textureLighting = "baked",
1516
+ atlasScale,
1517
+ autoCenter = false,
1518
+ className,
1519
+ style,
1520
+ children,
1521
+ position: _position,
1522
+ scale: _scale,
1523
+ rotation: _rotation,
1524
+ debugShowLabels: _debugShowLabels,
1525
+ debugShowBackfaces
1526
+ }) {
1527
+ const { store, sceneElRef } = useCameraContext();
1528
+ const cameraState = store.getState().cameraState;
1529
+ const localSceneRef = useCallback4(
1530
+ (el) => {
1531
+ sceneElRef.current = el;
1532
+ },
1533
+ [sceneElRef]
1534
+ );
1535
+ useEffect3(() => {
1536
+ const el = sceneElRef.current;
1537
+ if (!el) return;
1538
+ el.classList.toggle("polycss-debug-show-backfaces", !!debugShowBackfaces);
1539
+ }, [debugShowBackfaces, sceneElRef]);
1540
+ const injectedRef = useRef3(false);
1541
+ useEffect3(() => {
1542
+ if (injectedRef.current) return;
1543
+ if (typeof document !== "undefined") {
1544
+ injectPolyBaseStyles(document);
1545
+ injectedRef.current = true;
1546
+ }
1547
+ }, []);
1548
+ const inputPolygons = useMemo6(() => polygonsProp ?? [], [polygonsProp]);
1549
+ const centerInputPolygons = useMemo6(
1550
+ () => centerPolygonsProp ?? null,
1551
+ [centerPolygonsProp]
1552
+ );
1553
+ const { polygons, sceneBbox: renderSceneBbox } = usePolySceneContext(inputPolygons, {
1554
+ directionalLight
1555
+ });
1556
+ const { sceneBbox: centerSceneBbox } = usePolySceneContext(
1557
+ centerInputPolygons ?? inputPolygons,
1558
+ { directionalLight }
1559
+ );
1560
+ const sceneBbox = centerInputPolygons ? centerSceneBbox : renderSceneBbox;
1561
+ const sceneStyle = useMemo6(() => {
1562
+ const handle = createIsometricCamera2(cameraState);
1563
+ const cameraStyle = handle.getStyle();
1564
+ return {
1565
+ ["--scene-transform"]: cameraStyle.transform
1566
+ };
1567
+ }, [cameraState]);
1568
+ const computedClassName = `polycss-scene${className ? ` ${className}` : ""}`;
1569
+ const directionalForAtlas = textureLighting === "dynamic" ? void 0 : directionalLight;
1570
+ const ambientForAtlas = textureLighting === "dynamic" ? void 0 : ambientLight;
1571
+ const polyContext = useMemo6(() => {
1572
+ const tileSize = 50;
1573
+ return {
1574
+ tileSize,
1575
+ layerElevation: tileSize,
1576
+ directionalLight: directionalForAtlas,
1577
+ ambientLight: ambientForAtlas,
1578
+ textureLighting
1579
+ };
1580
+ }, [directionalForAtlas, ambientForAtlas, textureLighting]);
1581
+ const textureAtlasPlans = useMemo6(
1582
+ () => polygons.map((p, i) => computeTextureAtlasPlan(p, i, polyContext)),
1583
+ [polygons, polyContext]
1584
+ );
1585
+ const textureAtlas = useTextureAtlas(textureAtlasPlans, textureLighting, atlasScale);
1586
+ const dynamicLightVars = useMemo6(() => {
1587
+ if (textureLighting !== "dynamic") return null;
1588
+ const dir = directionalLight?.direction ?? [0.4, -0.7, 0.59];
1589
+ const len = Math.hypot(dir[0], dir[1], dir[2]) || 1;
1590
+ const lx = dir[0] / len, ly = dir[1] / len, lz = dir[2] / len;
1591
+ const lightRgb = parseHexColor(directionalLight?.color ?? "#ffffff")?.rgb ?? [255, 255, 255];
1592
+ const ambRgb = parseHexColor(ambientLight?.color ?? "#ffffff")?.rgb ?? [255, 255, 255];
1593
+ const lightIntensity = directionalLight?.intensity ?? 1;
1594
+ const ambientIntensity = ambientLight?.intensity ?? 0.4;
1595
+ const ch = (n) => (n / 255).toFixed(4);
1596
+ return {
1597
+ ["--plx"]: lx.toFixed(4),
1598
+ ["--ply"]: ly.toFixed(4),
1599
+ ["--plz"]: lz.toFixed(4),
1600
+ ["--plr"]: ch(lightRgb[0]),
1601
+ ["--plg"]: ch(lightRgb[1]),
1602
+ ["--plb"]: ch(lightRgb[2]),
1603
+ ["--pli"]: lightIntensity.toFixed(4),
1604
+ ["--par"]: ch(ambRgb[0]),
1605
+ ["--pag"]: ch(ambRgb[1]),
1606
+ ["--pab"]: ch(ambRgb[2]),
1607
+ ["--pai"]: ambientIntensity.toFixed(4)
1608
+ };
1609
+ }, [textureLighting, directionalLight, ambientLight]);
1610
+ const autoCenterTransform = useMemo6(() => {
1611
+ if (!autoCenter) return void 0;
1612
+ const cssX = (sceneBbox.min[1] + sceneBbox.max[1]) / 2 * polyContext.tileSize;
1613
+ const cssY = (sceneBbox.min[0] + sceneBbox.max[0]) / 2 * polyContext.tileSize;
1614
+ const cssZ = (sceneBbox.min[2] + sceneBbox.max[2]) / 2 * polyContext.layerElevation;
1615
+ return `translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
1616
+ }, [autoCenter, sceneBbox, polyContext.tileSize, polyContext.layerElevation]);
1617
+ const polyChildren = textureAtlas.entries.map((entry, index) => {
1618
+ if (entry) {
1619
+ return /* @__PURE__ */ jsx4(
1620
+ TextureAtlasPoly,
1621
+ {
1622
+ entry,
1623
+ page: textureAtlas.pages[entry.pageIndex],
1624
+ textureLighting
1625
+ },
1626
+ entry.index
1627
+ );
1628
+ }
1629
+ const plan = textureAtlasPlans[index];
1630
+ if (!plan || plan.texture) return null;
1631
+ return isSolidTrianglePlan(plan) ? /* @__PURE__ */ jsx4(TextureTrianglePoly, { entry: plan, textureLighting }, plan.index) : /* @__PURE__ */ jsx4(TextureBorderShapePoly, { entry: plan }, plan.index);
1632
+ });
1633
+ const sceneCtxValue = useMemo6(
1634
+ () => ({ textureLighting, directionalLight, ambientLight }),
1635
+ [textureLighting, directionalLight, ambientLight]
1636
+ );
1637
+ return /* @__PURE__ */ jsx4(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ jsx4(
1638
+ "div",
1639
+ {
1640
+ ref: localSceneRef,
1641
+ className: computedClassName,
1642
+ "data-polycss-lighting": textureLighting,
1643
+ "aria-hidden": "true",
1644
+ style: {
1645
+ ...sceneStyle,
1646
+ ...dynamicLightVars ?? null,
1647
+ ...style
1648
+ // No more --polycss-rows / --polycss-cols — CSS Grid was dropped
1649
+ // in Phase 4 (per §Design.4a).
1650
+ },
1651
+ children: autoCenterTransform ? /* @__PURE__ */ jsxs("div", { className: "polycss-offset", style: { ["--offset-transform"]: autoCenterTransform }, children: [
1652
+ polyChildren,
1653
+ children
1654
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1655
+ polyChildren,
1656
+ children
1657
+ ] })
1658
+ }
1659
+ ) });
1660
+ }
1661
+ var PolyScene = memo3(PolySceneInner);
1662
+
1663
+ // src/scene/PolyMesh.tsx
1664
+ import {
1665
+ forwardRef,
1666
+ useCallback as useCallback6,
1667
+ useContext as useContext3,
1668
+ useEffect as useEffect5,
1669
+ useImperativeHandle,
1670
+ useMemo as useMemo7,
1671
+ useRef as useRef5,
1672
+ useState as useState3
1673
+ } from "react";
1674
+ import { computeSceneBbox, inverseRotateVec3 } from "@layoutit/polycss-core";
1675
+
1676
+ // src/scene/useMesh.ts
1677
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
1678
+ import { loadMesh } from "@layoutit/polycss-core";
1679
+ var EMPTY_POLYGONS = [];
1680
+ var EMPTY_WARNINGS = [];
1681
+ function usePolyMesh(src, options) {
1682
+ const [state, setState] = useState2({
1683
+ polygons: EMPTY_POLYGONS,
1684
+ loading: !!src,
1685
+ error: null,
1686
+ warnings: EMPTY_WARNINGS
1687
+ });
1688
+ const activeResultRef = useRef4(null);
1689
+ const dispose = useCallback5(() => {
1690
+ const r = activeResultRef.current;
1691
+ if (r) {
1692
+ try {
1693
+ r.dispose();
1694
+ } catch {
1695
+ }
1696
+ activeResultRef.current = null;
1697
+ }
1698
+ }, []);
1699
+ useEffect4(() => {
1700
+ if (!src) {
1701
+ dispose();
1702
+ setState({
1703
+ polygons: EMPTY_POLYGONS,
1704
+ loading: false,
1705
+ error: null,
1706
+ warnings: EMPTY_WARNINGS
1707
+ });
1708
+ return;
1709
+ }
1710
+ let cancelled = false;
1711
+ setState((prev) => ({ ...prev, loading: true, error: null }));
1712
+ const prevResult = activeResultRef.current;
1713
+ loadMesh(src, options).then((result) => {
1714
+ if (cancelled) {
1715
+ try {
1716
+ result.dispose();
1717
+ } catch {
1718
+ }
1719
+ return;
1720
+ }
1721
+ if (prevResult) {
1722
+ try {
1723
+ prevResult.dispose();
1724
+ } catch {
1725
+ }
1726
+ }
1727
+ activeResultRef.current = result;
1728
+ setState({
1729
+ polygons: result.polygons,
1730
+ loading: false,
1731
+ error: null,
1732
+ warnings: result.warnings ?? EMPTY_WARNINGS
1733
+ });
1734
+ }).catch((err) => {
1735
+ if (cancelled) return;
1736
+ const error = err instanceof Error ? err : new Error(String(err));
1737
+ setState((prev) => ({
1738
+ polygons: prev.polygons,
1739
+ loading: false,
1740
+ error,
1741
+ warnings: prev.warnings
1742
+ }));
1743
+ });
1744
+ return () => {
1745
+ cancelled = true;
1746
+ };
1747
+ }, [src, dispose]);
1748
+ useEffect4(() => {
1749
+ return () => dispose();
1750
+ }, [dispose]);
1751
+ return {
1752
+ polygons: state.polygons,
1753
+ loading: state.loading,
1754
+ error: state.error,
1755
+ warnings: state.warnings,
1756
+ dispose
1757
+ };
1758
+ }
1759
+
1760
+ // src/scene/events.ts
1761
+ var MESH_REGISTRY = /* @__PURE__ */ new WeakMap();
1762
+ function registerMeshElement(el, handle) {
1763
+ MESH_REGISTRY.set(el, handle);
1764
+ }
1765
+ function unregisterMeshElement(el) {
1766
+ MESH_REGISTRY.delete(el);
1767
+ }
1768
+ function findPolyMeshHandle(el) {
1769
+ let cur = el;
1770
+ while (cur) {
1771
+ if (cur instanceof HTMLElement) {
1772
+ const h = MESH_REGISTRY.get(cur);
1773
+ if (h) return h;
1774
+ }
1775
+ cur = cur.parentElement;
1776
+ }
1777
+ return null;
1778
+ }
1779
+ function pointInMeshElement(meshEl, clientX, clientY) {
1780
+ const polys = Array.from(meshEl.querySelectorAll("i,b,s,u"));
1781
+ for (const p of polys) {
1782
+ const r = p.getBoundingClientRect();
1783
+ if (r.width <= 0 || r.height <= 0) continue;
1784
+ if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) {
1785
+ return true;
1786
+ }
1787
+ }
1788
+ return false;
1789
+ }
1790
+ function findMeshUnderPoint(clientX, clientY, filter) {
1791
+ if (typeof document === "undefined") return null;
1792
+ const meshEls = Array.from(
1793
+ document.querySelectorAll(".polycss-mesh")
1794
+ );
1795
+ for (const meshEl of meshEls) {
1796
+ if (filter && !filter(meshEl)) continue;
1797
+ const handle = findPolyMeshHandle(meshEl);
1798
+ if (!handle) continue;
1799
+ if (pointInMeshElement(meshEl, clientX, clientY)) return handle;
1800
+ }
1801
+ return null;
1802
+ }
1803
+
1804
+ // src/scene/PolyMesh.tsx
1805
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
1806
+ function buildTransform(position, scale, rotation) {
1807
+ const parts = [];
1808
+ if (position) {
1809
+ parts.push(`translate3d(${position[0]}px, ${position[1]}px, ${position[2]}px)`);
1810
+ }
1811
+ if (scale !== void 0) {
1812
+ if (typeof scale === "number") {
1813
+ if (scale !== 1) parts.push(`scale3d(${scale}, ${scale}, ${scale})`);
1814
+ } else {
1815
+ parts.push(`scale3d(${scale[0]}, ${scale[1]}, ${scale[2]})`);
1816
+ }
1817
+ }
1818
+ if (rotation) {
1819
+ if (rotation[0]) parts.push(`rotateX(${rotation[0]}deg)`);
1820
+ if (rotation[1]) parts.push(`rotateY(${rotation[1]}deg)`);
1821
+ if (rotation[2]) parts.push(`rotateZ(${rotation[2]}deg)`);
1822
+ }
1823
+ return parts.length > 0 ? parts.join(" ") : void 0;
1824
+ }
1825
+ function recenterPolygons(polygons) {
1826
+ if (polygons.length === 0) return polygons;
1827
+ const bbox = computeSceneBbox(polygons);
1828
+ const cx = (bbox.min[0] + bbox.max[0]) / 2;
1829
+ const cy = (bbox.min[1] + bbox.max[1]) / 2;
1830
+ const cz = (bbox.min[2] + bbox.max[2]) / 2;
1831
+ if (cx === 0 && cy === 0 && cz === 0) return polygons;
1832
+ const shift = (v) => [v[0] - cx, v[1] - cy, v[2] - cz];
1833
+ return polygons.map((p) => ({
1834
+ ...p,
1835
+ vertices: p.vertices.map(shift),
1836
+ ...p.textureTriangles?.length ? {
1837
+ textureTriangles: p.textureTriangles.map((triangle) => ({
1838
+ ...triangle,
1839
+ vertices: triangle.vertices.map(shift)
1840
+ }))
1841
+ } : null
1842
+ }));
1843
+ }
1844
+ var PolyMesh = forwardRef(function PolyMesh2({
1845
+ id,
1846
+ src,
1847
+ mtl,
1848
+ polygons: polygonsProp,
1849
+ autoCenter,
1850
+ textureLighting,
1851
+ atlasScale,
1852
+ children,
1853
+ fallback,
1854
+ errorFallback,
1855
+ parseOptions,
1856
+ position,
1857
+ scale,
1858
+ rotation,
1859
+ className,
1860
+ style,
1861
+ onClick,
1862
+ onContextMenu,
1863
+ onDoubleClick,
1864
+ onWheel,
1865
+ onPointerDown,
1866
+ onPointerUp,
1867
+ onPointerMove,
1868
+ onPointerOver,
1869
+ onPointerOut,
1870
+ onPointerEnter,
1871
+ onPointerLeave,
1872
+ onPointerCancel
1873
+ }, forwardedRef) {
1874
+ const mergedOptions = useMemo7(() => {
1875
+ if (!mtl && !parseOptions) return void 0;
1876
+ return { ...parseOptions ?? {}, ...mtl ? { mtlUrl: mtl } : {} };
1877
+ }, [mtl, parseOptions]);
1878
+ const fetched = usePolyMesh(src ?? "", mergedOptions);
1879
+ const sourcePolygons = src ? fetched.polygons : polygonsProp ?? [];
1880
+ const polygons = useMemo7(
1881
+ () => autoCenter ? recenterPolygons(sourcePolygons) : sourcePolygons,
1882
+ [sourcePolygons, autoCenter]
1883
+ );
1884
+ const transform = buildTransform(position, scale, rotation);
1885
+ const wrapperRef = useRef5(null);
1886
+ const propsRef = useRef5({ position, scale, rotation });
1887
+ propsRef.current = { position, scale, rotation };
1888
+ const polygonsRef = useRef5(polygons);
1889
+ polygonsRef.current = polygons;
1890
+ const [bakedRotation, setBakedRotation] = useState3(rotation);
1891
+ const handle = useMemo7(() => ({
1892
+ get element() {
1893
+ return wrapperRef.current;
1894
+ },
1895
+ id,
1896
+ getPosition: () => propsRef.current.position,
1897
+ getRotation: () => propsRef.current.rotation,
1898
+ getScale: () => propsRef.current.scale,
1899
+ getPolygons: () => polygonsRef.current,
1900
+ rebakeAtlas: () => setBakedRotation(propsRef.current.rotation)
1901
+ }), [id]);
1902
+ useImperativeHandle(forwardedRef, () => handle, [handle]);
1903
+ useEffect5(() => {
1904
+ const el = wrapperRef.current;
1905
+ if (!el) return;
1906
+ registerMeshElement(el, handle);
1907
+ return () => unregisterMeshElement(el);
1908
+ }, [handle]);
1909
+ const cameraCtx = useContext3(PolyCameraContext);
1910
+ const cameraElRef = cameraCtx?.cameraElRef ?? null;
1911
+ const pointerDownAtRef = useRef5(null);
1912
+ const makeEvent = useCallback6(
1913
+ function makeEvent2(nativeEvent, clientX, clientY) {
1914
+ const intersections = [];
1915
+ if (typeof document !== "undefined" && typeof document.elementsFromPoint === "function") {
1916
+ const stacked = document.elementsFromPoint(clientX, clientY);
1917
+ const seen = /* @__PURE__ */ new Set();
1918
+ for (const el of stacked) {
1919
+ const h = findPolyMeshHandle(el);
1920
+ if (h && !seen.has(h)) {
1921
+ seen.add(h);
1922
+ intersections.push({ object: h });
1923
+ }
1924
+ }
1925
+ }
1926
+ let nx = 0;
1927
+ let ny = 0;
1928
+ const camEl = cameraElRef?.current;
1929
+ if (camEl) {
1930
+ const r = camEl.getBoundingClientRect();
1931
+ if (r.width > 0 && r.height > 0) {
1932
+ nx = (clientX - r.left) / r.width * 2 - 1;
1933
+ ny = -((clientY - r.top) / r.height * 2 - 1);
1934
+ }
1935
+ }
1936
+ let delta = 0;
1937
+ const pd = pointerDownAtRef.current;
1938
+ if (pd) delta = Math.hypot(clientX - pd.x, clientY - pd.y);
1939
+ return {
1940
+ object: intersections[0]?.object ?? handle,
1941
+ eventObject: handle,
1942
+ intersections,
1943
+ pointer: { x: nx, y: ny },
1944
+ delta,
1945
+ nativeEvent,
1946
+ stopPropagation: () => nativeEvent.stopPropagation()
1947
+ };
1948
+ },
1949
+ [cameraElRef, handle]
1950
+ );
1951
+ const wrapperHandlers = useMemo7(() => {
1952
+ const dispatch = (polyHandler, reactEvent, nativeEvent, clientX, clientY) => {
1953
+ if (!polyHandler) return;
1954
+ const polyEvent = makeEvent(nativeEvent, clientX, clientY);
1955
+ const originalStop = polyEvent.stopPropagation;
1956
+ polyEvent.stopPropagation = () => {
1957
+ originalStop();
1958
+ reactEvent.stopPropagation();
1959
+ };
1960
+ polyHandler(polyEvent);
1961
+ };
1962
+ const out = {};
1963
+ if (onClick) {
1964
+ out.onClick = (e) => dispatch(onClick, e, e.nativeEvent, e.clientX, e.clientY);
1965
+ }
1966
+ if (onContextMenu) {
1967
+ out.onContextMenu = (e) => dispatch(onContextMenu, e, e.nativeEvent, e.clientX, e.clientY);
1968
+ }
1969
+ if (onDoubleClick) {
1970
+ out.onDoubleClick = (e) => dispatch(onDoubleClick, e, e.nativeEvent, e.clientX, e.clientY);
1971
+ }
1972
+ if (onWheel) {
1973
+ out.onWheel = (e) => dispatch(onWheel, e, e.nativeEvent, e.clientX, e.clientY);
1974
+ }
1975
+ if (onPointerDown) {
1976
+ out.onPointerDown = (e) => {
1977
+ pointerDownAtRef.current = { x: e.clientX, y: e.clientY };
1978
+ dispatch(onPointerDown, e, e.nativeEvent, e.clientX, e.clientY);
1979
+ };
1980
+ } else {
1981
+ out.onPointerDown = (e) => {
1982
+ pointerDownAtRef.current = { x: e.clientX, y: e.clientY };
1983
+ };
1984
+ }
1985
+ if (onPointerUp) {
1986
+ out.onPointerUp = (e) => {
1987
+ dispatch(onPointerUp, e, e.nativeEvent, e.clientX, e.clientY);
1988
+ pointerDownAtRef.current = null;
1989
+ };
1990
+ } else {
1991
+ out.onPointerUp = () => {
1992
+ pointerDownAtRef.current = null;
1993
+ };
1994
+ }
1995
+ if (onPointerMove) {
1996
+ out.onPointerMove = (e) => dispatch(onPointerMove, e, e.nativeEvent, e.clientX, e.clientY);
1997
+ }
1998
+ if (onPointerOver || onPointerEnter) {
1999
+ out.onPointerEnter = (e) => {
2000
+ if (onPointerOver) dispatch(onPointerOver, e, e.nativeEvent, e.clientX, e.clientY);
2001
+ if (onPointerEnter) dispatch(onPointerEnter, e, e.nativeEvent, e.clientX, e.clientY);
2002
+ };
2003
+ }
2004
+ if (onPointerOut || onPointerLeave) {
2005
+ out.onPointerLeave = (e) => {
2006
+ if (onPointerOut) dispatch(onPointerOut, e, e.nativeEvent, e.clientX, e.clientY);
2007
+ if (onPointerLeave) dispatch(onPointerLeave, e, e.nativeEvent, e.clientX, e.clientY);
2008
+ };
2009
+ }
2010
+ if (onPointerCancel) {
2011
+ out.onPointerCancel = (e) => {
2012
+ dispatch(onPointerCancel, e, e.nativeEvent, e.clientX, e.clientY);
2013
+ pointerDownAtRef.current = null;
2014
+ };
2015
+ }
2016
+ return out;
2017
+ }, [
2018
+ makeEvent,
2019
+ onClick,
2020
+ onContextMenu,
2021
+ onDoubleClick,
2022
+ onWheel,
2023
+ onPointerDown,
2024
+ onPointerUp,
2025
+ onPointerMove,
2026
+ onPointerOver,
2027
+ onPointerOut,
2028
+ onPointerEnter,
2029
+ onPointerLeave,
2030
+ onPointerCancel
2031
+ ]);
2032
+ const sceneCtx = usePolySceneContext2();
2033
+ const effectiveTextureLighting = textureLighting ?? sceneCtx?.textureLighting ?? "baked";
2034
+ const effectiveDirectional = effectiveTextureLighting === "dynamic" ? void 0 : sceneCtx?.directionalLight;
2035
+ const effectiveAmbient = effectiveTextureLighting === "dynamic" ? void 0 : sceneCtx?.ambientLight;
2036
+ const sceneDirectionalLight = sceneCtx?.directionalLight;
2037
+ const dynamicLightOverride = useMemo7(() => {
2038
+ if (effectiveTextureLighting !== "dynamic") return null;
2039
+ if (!rotation || rotation[0] === 0 && rotation[1] === 0 && rotation[2] === 0) return null;
2040
+ if (!sceneDirectionalLight) return null;
2041
+ const dir = sceneDirectionalLight.direction;
2042
+ const localDir = inverseRotateVec3(dir, rotation);
2043
+ const len = Math.hypot(localDir[0], localDir[1], localDir[2]) || 1;
2044
+ return {
2045
+ ["--plx"]: (localDir[0] / len).toFixed(4),
2046
+ ["--ply"]: (localDir[1] / len).toFixed(4),
2047
+ ["--plz"]: (localDir[2] / len).toFixed(4)
2048
+ };
2049
+ }, [effectiveTextureLighting, rotation, sceneDirectionalLight]);
2050
+ const wrapperStyle = {
2051
+ transform,
2052
+ ...dynamicLightOverride,
2053
+ ...style
2054
+ };
2055
+ const bakedDirectional = useMemo7(() => {
2056
+ if (!effectiveDirectional) return effectiveDirectional;
2057
+ const rot = bakedRotation ?? [0, 0, 0];
2058
+ if (rot[0] === 0 && rot[1] === 0 && rot[2] === 0) return effectiveDirectional;
2059
+ return {
2060
+ ...effectiveDirectional,
2061
+ direction: inverseRotateVec3(effectiveDirectional.direction, rot)
2062
+ };
2063
+ }, [effectiveDirectional, bakedRotation]);
2064
+ const atlasPlans = useMemo7(
2065
+ () => !children ? polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
2066
+ directionalLight: bakedDirectional,
2067
+ ambientLight: effectiveAmbient
2068
+ })) : [],
2069
+ [children, polygons, bakedDirectional, effectiveAmbient]
2070
+ );
2071
+ const textureAtlas = useTextureAtlas(
2072
+ atlasPlans,
2073
+ effectiveTextureLighting,
2074
+ atlasScale
2075
+ );
2076
+ const renderedPolygons = children ? polygons.map((p, i) => (
2077
+ // Render-prop: caller controls how each polygon renders. We still
2078
+ // wrap in a fragment with key so React reconciliation works.
2079
+ /* @__PURE__ */ jsx5(RenderPropPolygon, { polygon: p, index: i, children }, i)
2080
+ )) : textureAtlas.entries.map((entry, index) => {
2081
+ if (entry) {
2082
+ return /* @__PURE__ */ jsx5(
2083
+ TextureAtlasPoly,
2084
+ {
2085
+ entry,
2086
+ page: textureAtlas.pages[entry.pageIndex],
2087
+ textureLighting: effectiveTextureLighting
2088
+ },
2089
+ entry.index
2090
+ );
2091
+ }
2092
+ const plan = atlasPlans[index];
2093
+ if (!plan || plan.texture) return null;
2094
+ return isSolidTrianglePlan(plan) ? /* @__PURE__ */ jsx5(TextureTrianglePoly, { entry: plan, textureLighting: effectiveTextureLighting }, plan.index) : /* @__PURE__ */ jsx5(TextureBorderShapePoly, { entry: plan }, plan.index);
2095
+ });
2096
+ if (src) {
2097
+ if (fetched.loading && fetched.polygons.length === 0) {
2098
+ return /* @__PURE__ */ jsx5(
2099
+ "div",
2100
+ {
2101
+ ref: wrapperRef,
2102
+ "data-poly-mesh-id": id,
2103
+ className: `polycss-mesh polycss-mesh-loading${className ? ` ${className}` : ""}`,
2104
+ style: wrapperStyle,
2105
+ ...wrapperHandlers,
2106
+ children: fallback ?? null
2107
+ }
2108
+ );
2109
+ }
2110
+ if (fetched.error && fetched.polygons.length === 0) {
2111
+ return /* @__PURE__ */ jsx5(
2112
+ "div",
2113
+ {
2114
+ ref: wrapperRef,
2115
+ "data-poly-mesh-id": id,
2116
+ className: `polycss-mesh polycss-mesh-error${className ? ` ${className}` : ""}`,
2117
+ style: wrapperStyle,
2118
+ ...wrapperHandlers,
2119
+ children: errorFallback ? errorFallback(fetched.error) : null
2120
+ }
2121
+ );
2122
+ }
2123
+ }
2124
+ return /* @__PURE__ */ jsx5(
2125
+ "div",
2126
+ {
2127
+ ref: wrapperRef,
2128
+ "data-poly-mesh-id": id,
2129
+ className: `polycss-mesh${className ? ` ${className}` : ""}`,
2130
+ style: wrapperStyle,
2131
+ ...wrapperHandlers,
2132
+ children: renderedPolygons
2133
+ }
2134
+ );
2135
+ });
2136
+ function RenderPropPolygon({
2137
+ polygon,
2138
+ index,
2139
+ children
2140
+ }) {
2141
+ return /* @__PURE__ */ jsx5(Fragment2, { children: children(polygon, index) });
2142
+ }
2143
+
2144
+ // src/scene/usePolyMaterial.ts
2145
+ import { useMemo as useMemo8 } from "react";
2146
+ function usePolyMaterial(options) {
2147
+ return useMemo8(
2148
+ () => ({ texture: options.texture, key: options.key }),
2149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2150
+ [options.texture, options.key]
2151
+ );
2152
+ }
2153
+
2154
+ // src/shapes/Poly.tsx
2155
+ import { memo as memo4, useMemo as useMemo9 } from "react";
2156
+ import { jsx as jsx6 } from "react/jsx-runtime";
2157
+ function isAxisAlignedRectUVs(uvs) {
2158
+ if (uvs.length !== 4) return null;
2159
+ const us = [...new Set(uvs.map((uv) => uv[0]))].sort((a, b) => a - b);
2160
+ const vs = [...new Set(uvs.map((uv) => uv[1]))].sort((a, b) => a - b);
2161
+ if (us.length !== 2 || vs.length !== 2) return null;
2162
+ const corners = /* @__PURE__ */ new Set([
2163
+ `${us[0]},${vs[0]}`,
2164
+ `${us[0]},${vs[1]}`,
2165
+ `${us[1]},${vs[0]}`,
2166
+ `${us[1]},${vs[1]}`
2167
+ ]);
2168
+ for (const uv of uvs) {
2169
+ if (!corners.has(`${uv[0]},${uv[1]}`)) return null;
2170
+ }
2171
+ return { u0: us[0], u1: us[1], v0: vs[0], v1: vs[1] };
2172
+ }
2173
+ function MaterialDirectPoly({
2174
+ plan,
2175
+ material,
2176
+ uvRect,
2177
+ className,
2178
+ style: styleProp,
2179
+ domAttrs,
2180
+ domEventHandlers,
2181
+ pointerEvents = "auto"
2182
+ }) {
2183
+ const { u0, u1, v0, v1 } = uvRect;
2184
+ const du = u1 - u0;
2185
+ const dv = v1 - v0;
2186
+ const sourceW = plan.canvasW / du;
2187
+ const sourceH = plan.canvasH / dv;
2188
+ const vMax = Math.max(v0, v1);
2189
+ const offsetX = u0 * sourceW;
2190
+ const offsetY = (1 - vMax) * sourceH;
2191
+ const style = {
2192
+ width: plan.canvasW,
2193
+ height: plan.canvasH,
2194
+ transform: `matrix3d(${plan.matrix})`,
2195
+ backgroundImage: `url(${material.texture})`,
2196
+ backgroundSize: `${sourceW}px ${sourceH}px`,
2197
+ backgroundPosition: `-${offsetX}px -${offsetY}px`,
2198
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
2199
+ ...styleProp
2200
+ };
2201
+ const dataAttrs = plan.polygon.data ? Object.fromEntries(
2202
+ Object.entries(plan.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
2203
+ ) : {};
2204
+ const elementClassName = className?.trim() || void 0;
2205
+ return /* @__PURE__ */ jsx6(
2206
+ "i",
2207
+ {
2208
+ className: elementClassName,
2209
+ style,
2210
+ ...domEventHandlers,
2211
+ ...dataAttrs,
2212
+ ...domAttrs
2213
+ }
2214
+ );
2215
+ }
2216
+ function PolyInner({
2217
+ vertices,
2218
+ color,
2219
+ texture,
2220
+ uvs,
2221
+ data,
2222
+ material,
2223
+ position,
2224
+ scale,
2225
+ rotation,
2226
+ className,
2227
+ style: styleProp,
2228
+ id,
2229
+ onClick,
2230
+ onDoubleClick,
2231
+ onMouseEnter,
2232
+ onMouseLeave,
2233
+ onMouseMove,
2234
+ onPointerDown,
2235
+ onPointerUp,
2236
+ onPointerEnter,
2237
+ onPointerLeave,
2238
+ onFocus,
2239
+ onBlur,
2240
+ onKeyDown,
2241
+ tabIndex,
2242
+ role,
2243
+ "aria-label": ariaLabel,
2244
+ "aria-hidden": ariaHidden,
2245
+ pointerEvents: pointerEventsProp,
2246
+ context,
2247
+ textureLighting: textureLightingProp,
2248
+ atlasScale: atlasScaleProp,
2249
+ baseColor: baseColorProp,
2250
+ ...dataAttrs
2251
+ }) {
2252
+ const tileSize = context?.tileSize ?? 50;
2253
+ const layerElevation = context?.layerElevation ?? tileSize;
2254
+ const textureLighting = textureLightingProp ?? context?.textureLighting ?? "baked";
2255
+ const atlasScale = atlasScaleProp ?? context?.atlasScale;
2256
+ const polygonColor = baseColorProp ?? color;
2257
+ const effectiveTexture = material?.texture ?? texture;
2258
+ const atlasPlan = useMemo9(
2259
+ () => computeTextureAtlasPlan(
2260
+ { vertices, color: polygonColor, texture: effectiveTexture, uvs, data },
2261
+ 0,
2262
+ {
2263
+ tileSize,
2264
+ layerElevation,
2265
+ directionalLight: context?.directionalLight
2266
+ }
2267
+ ),
2268
+ [
2269
+ vertices,
2270
+ polygonColor,
2271
+ effectiveTexture,
2272
+ uvs,
2273
+ data,
2274
+ tileSize,
2275
+ layerElevation,
2276
+ context?.directionalLight
2277
+ ]
2278
+ );
2279
+ const materialUvRect = useMemo9(
2280
+ () => material && uvs ? isAxisAlignedRectUVs(uvs) : null,
2281
+ [material, uvs]
2282
+ );
2283
+ const atlasPlans = useMemo9(
2284
+ () => materialUvRect ? [] : [atlasPlan],
2285
+ [materialUvRect, atlasPlan]
2286
+ );
2287
+ const textureAtlas = useTextureAtlas(atlasPlans, textureLighting, atlasScale);
2288
+ const domEventHandlers = {
2289
+ onClick,
2290
+ onDoubleClick,
2291
+ onMouseEnter,
2292
+ onMouseLeave,
2293
+ onMouseMove,
2294
+ onPointerDown,
2295
+ onPointerUp,
2296
+ onPointerEnter,
2297
+ onPointerLeave,
2298
+ onFocus,
2299
+ onBlur,
2300
+ onKeyDown
2301
+ };
2302
+ const domAttrs = {
2303
+ id,
2304
+ tabIndex,
2305
+ role,
2306
+ "aria-label": ariaLabel,
2307
+ "aria-hidden": ariaHidden,
2308
+ ...Object.fromEntries(
2309
+ Object.entries(dataAttrs).filter(([k]) => k.startsWith("data-"))
2310
+ ),
2311
+ ...data ? Object.fromEntries(
2312
+ Object.entries(data).map(([k, v]) => [`data-${k}`, String(v)])
2313
+ ) : {}
2314
+ };
2315
+ const transformParts = [];
2316
+ if (position) {
2317
+ transformParts.push(
2318
+ `translate3d(${position[0]}px, ${position[1]}px, ${position[2]}px)`
2319
+ );
2320
+ }
2321
+ if (scale !== void 0) {
2322
+ if (typeof scale === "number") {
2323
+ if (scale !== 1) transformParts.push(`scale3d(${scale}, ${scale}, ${scale})`);
2324
+ } else {
2325
+ transformParts.push(`scale3d(${scale[0]}, ${scale[1]}, ${scale[2]})`);
2326
+ }
2327
+ }
2328
+ if (rotation) {
2329
+ if (rotation[0]) transformParts.push(`rotateX(${rotation[0]}deg)`);
2330
+ if (rotation[1]) transformParts.push(`rotateY(${rotation[1]}deg)`);
2331
+ if (rotation[2]) transformParts.push(`rotateZ(${rotation[2]}deg)`);
2332
+ }
2333
+ const wrapperTransform = transformParts.length > 0 ? transformParts.join(" ") : void 0;
2334
+ let front = null;
2335
+ if (materialUvRect && material && atlasPlan) {
2336
+ front = /* @__PURE__ */ jsx6(
2337
+ MaterialDirectPoly,
2338
+ {
2339
+ plan: atlasPlan,
2340
+ material,
2341
+ uvRect: materialUvRect,
2342
+ className,
2343
+ style: styleProp,
2344
+ domAttrs,
2345
+ domEventHandlers,
2346
+ pointerEvents: pointerEventsProp ?? "auto"
2347
+ }
2348
+ );
2349
+ } else {
2350
+ const atlasEntry = textureAtlas.entries[0];
2351
+ if (atlasEntry) {
2352
+ front = /* @__PURE__ */ jsx6(
2353
+ TextureAtlasPoly,
2354
+ {
2355
+ entry: atlasEntry,
2356
+ page: textureAtlas.pages[atlasEntry.pageIndex],
2357
+ textureLighting,
2358
+ className,
2359
+ style: styleProp,
2360
+ domAttrs,
2361
+ domEventHandlers,
2362
+ pointerEvents: pointerEventsProp ?? "auto"
2363
+ }
2364
+ );
2365
+ } else if (atlasPlan && !atlasPlan.texture) {
2366
+ front = isSolidTrianglePlan(atlasPlan) ? /* @__PURE__ */ jsx6(
2367
+ TextureTrianglePoly,
2368
+ {
2369
+ entry: atlasPlan,
2370
+ textureLighting,
2371
+ className,
2372
+ style: styleProp,
2373
+ domAttrs,
2374
+ domEventHandlers,
2375
+ pointerEvents: pointerEventsProp ?? "auto"
2376
+ }
2377
+ ) : /* @__PURE__ */ jsx6(
2378
+ TextureBorderShapePoly,
2379
+ {
2380
+ entry: atlasPlan,
2381
+ className,
2382
+ style: styleProp,
2383
+ domAttrs,
2384
+ domEventHandlers,
2385
+ pointerEvents: pointerEventsProp ?? "auto"
2386
+ }
2387
+ );
2388
+ }
2389
+ }
2390
+ if (!front) return null;
2391
+ if (!wrapperTransform) return front;
2392
+ return /* @__PURE__ */ jsx6(
2393
+ "div",
2394
+ {
2395
+ className: "polycss-poly-wrapper",
2396
+ style: {
2397
+ position: "absolute",
2398
+ transformStyle: "preserve-3d",
2399
+ transform: wrapperTransform
2400
+ },
2401
+ children: front
2402
+ }
2403
+ );
2404
+ }
2405
+ var Poly = memo4(PolyInner);
2406
+
2407
+ // src/controls/PolyOrbitControls.tsx
2408
+ import { useEffect as useEffect6, useRef as useRef6 } from "react";
2409
+
2410
+ // src/controls/sharedControls.ts
2411
+ import { BASE_TILE as BASE_TILE2 } from "@layoutit/polycss-core";
2412
+ var POINTER_DRAG_SPEED = 4;
2413
+ function invertFactor(invert) {
2414
+ if (invert === true) return -1;
2415
+ if (invert === void 0 || invert === false) return 1;
2416
+ return invert;
2417
+ }
2418
+ function applyOrbit(dx, dy, s, handle, invert) {
2419
+ const f = invertFactor(invert);
2420
+ const dX = dx / POINTER_DRAG_SPEED * f;
2421
+ const dY = dy / POINTER_DRAG_SPEED * f;
2422
+ const rotX = Math.max(0, Math.min(100, s.rotX - dY));
2423
+ const rotY = ((s.rotY - dX) % 360 + 360) % 360;
2424
+ handle.update({ rotX, rotY });
2425
+ }
2426
+ function applyPan(dx, dy, s, handle, _invert) {
2427
+ const z = Math.max(0.01, s.zoom);
2428
+ const cosRotXRaw = Math.cos(s.rotX * Math.PI / 180);
2429
+ const cosRotX = cosRotXRaw >= 0 ? Math.max(0.1, cosRotXRaw) : Math.min(-0.1, cosRotXRaw);
2430
+ const cZ = Math.cos(s.rotY * Math.PI / 180);
2431
+ const sZ = Math.sin(s.rotY * Math.PI / 180);
2432
+ const k = z * BASE_TILE2;
2433
+ const targetD0 = (dx * sZ - dy * cZ / cosRotX) / k;
2434
+ const targetD1 = -(dx * cZ + dy * sZ / cosRotX) / k;
2435
+ const t = s.target;
2436
+ handle.update({ target: [t[0] + targetD0, t[1] + targetD1, t[2]] });
2437
+ }
2438
+ var buildOrbitControls = {
2439
+ applyOrbit,
2440
+ applyPan,
2441
+ invertFactor
2442
+ };
2443
+
2444
+ // src/controls/controlsEffects.ts
2445
+ var WHEEL_IDLE_END_MS = 150;
2446
+ var ZOOM_STEP = 513e-6;
2447
+ var PINCH_AMP = 10;
2448
+ var SCROLL_AMP = 3;
2449
+ var ANIM_FRAME_MS = 16.67;
2450
+ var ANIM_DT_CLAMP_MS = 50;
2451
+ var DEFAULT_ANIMATE_SPEED = 0.3;
2452
+ var DOLLY_STEP = 0.05;
2453
+ function makeWheelEffect({
2454
+ wheel,
2455
+ dollyRef,
2456
+ wheelRef,
2457
+ zoomMinRef,
2458
+ zoomMaxRef,
2459
+ distanceMinRef,
2460
+ distanceMaxRef,
2461
+ cameraElRef,
2462
+ cameraRef,
2463
+ applyTransformDirect,
2464
+ store,
2465
+ fireStart,
2466
+ fireChange,
2467
+ fireEnd
2468
+ }) {
2469
+ if (!wheel) return;
2470
+ const el = cameraElRef.current;
2471
+ if (!el) return;
2472
+ let wheelActive = false;
2473
+ let wheelIdleTimer = null;
2474
+ const onWheel = (e) => {
2475
+ if (!wheelRef.current) return;
2476
+ e.preventDefault();
2477
+ const lineFactor = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
2478
+ let delta = e.deltaY * lineFactor;
2479
+ if (e.ctrlKey) delta *= PINCH_AMP;
2480
+ else delta *= SCROLL_AMP;
2481
+ const handle = cameraRef.current;
2482
+ if (dollyRef.current) {
2483
+ const nextDist = Math.max(
2484
+ distanceMinRef.current,
2485
+ Math.min(distanceMaxRef.current, handle.state.distance + delta * DOLLY_STEP)
2486
+ );
2487
+ handle.update({ distance: nextDist });
2488
+ } else {
2489
+ const factor = Math.exp(-delta * ZOOM_STEP);
2490
+ const next = Math.max(
2491
+ zoomMinRef.current,
2492
+ Math.min(zoomMaxRef.current, handle.state.zoom * factor)
2493
+ );
2494
+ handle.update({ zoom: next });
2495
+ }
2496
+ applyTransformDirect();
2497
+ store.updateCameraFromRef(handle);
2498
+ if (!wheelActive) {
2499
+ wheelActive = true;
2500
+ fireStart();
2501
+ }
2502
+ fireChange();
2503
+ if (wheelIdleTimer !== null) clearTimeout(wheelIdleTimer);
2504
+ wheelIdleTimer = setTimeout(() => {
2505
+ wheelIdleTimer = null;
2506
+ wheelActive = false;
2507
+ fireEnd();
2508
+ }, WHEEL_IDLE_END_MS);
2509
+ };
2510
+ el.addEventListener("wheel", onWheel, { passive: false });
2511
+ return () => {
2512
+ el.removeEventListener("wheel", onWheel);
2513
+ if (wheelIdleTimer !== null) clearTimeout(wheelIdleTimer);
2514
+ };
2515
+ }
2516
+ function makeAnimateEffect({
2517
+ animateOn,
2518
+ animateRef,
2519
+ animationPausedShared,
2520
+ applyTransformDirect,
2521
+ cameraRef,
2522
+ store,
2523
+ fireChange
2524
+ }) {
2525
+ if (!animateOn) return;
2526
+ let rafId = null;
2527
+ let stopped = false;
2528
+ let lastTime = 0;
2529
+ const tick = (now) => {
2530
+ if (stopped) return;
2531
+ const a = animateRef.current;
2532
+ if (!a) {
2533
+ rafId = requestAnimationFrame(tick);
2534
+ return;
2535
+ }
2536
+ if (!animationPausedShared.value) {
2537
+ const dt = Math.min(ANIM_DT_CLAMP_MS, lastTime ? now - lastTime : ANIM_FRAME_MS);
2538
+ lastTime = now;
2539
+ const speed = a.speed ?? DEFAULT_ANIMATE_SPEED;
2540
+ const delta = speed * (dt / ANIM_FRAME_MS);
2541
+ const handle = cameraRef.current;
2542
+ const s = handle.state;
2543
+ if (a.axis === "x") {
2544
+ const rotX = ((s.rotX + delta) % 360 + 360) % 360;
2545
+ handle.update({ rotX });
2546
+ } else {
2547
+ const rotY = ((s.rotY + delta) % 360 + 360) % 360;
2548
+ handle.update({ rotY });
2549
+ }
2550
+ applyTransformDirect();
2551
+ store.updateCameraFromRef(handle);
2552
+ fireChange();
2553
+ } else {
2554
+ lastTime = now;
2555
+ }
2556
+ rafId = requestAnimationFrame(tick);
2557
+ };
2558
+ rafId = requestAnimationFrame(tick);
2559
+ return () => {
2560
+ stopped = true;
2561
+ if (rafId !== null) cancelAnimationFrame(rafId);
2562
+ };
2563
+ }
2564
+
2565
+ // src/controls/PolyOrbitControls.tsx
2566
+ function PolyOrbitControls({
2567
+ drag = true,
2568
+ wheel = true,
2569
+ dolly = false,
2570
+ invert = false,
2571
+ minZoom = 0.1,
2572
+ maxZoom = 10,
2573
+ minDistance = 0,
2574
+ maxDistance = 5e3,
2575
+ animate = false,
2576
+ onChange,
2577
+ onInteractionStart,
2578
+ onInteractionEnd
2579
+ }) {
2580
+ const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
2581
+ const dragRef = useRef6(drag);
2582
+ const wheelRef = useRef6(wheel);
2583
+ const dollyRef = useRef6(dolly);
2584
+ const invertRef = useRef6(invert);
2585
+ const zoomMinRef = useRef6(minZoom);
2586
+ const zoomMaxRef = useRef6(maxZoom);
2587
+ const distanceMinRef = useRef6(minDistance);
2588
+ const distanceMaxRef = useRef6(maxDistance);
2589
+ const animateRef = useRef6(animate);
2590
+ const onChangeRef = useRef6(onChange);
2591
+ const onInteractionStartRef = useRef6(onInteractionStart);
2592
+ const onInteractionEndRef = useRef6(onInteractionEnd);
2593
+ useEffect6(() => {
2594
+ dragRef.current = drag;
2595
+ wheelRef.current = wheel;
2596
+ dollyRef.current = dolly;
2597
+ invertRef.current = invert;
2598
+ zoomMinRef.current = minZoom;
2599
+ zoomMaxRef.current = maxZoom;
2600
+ distanceMinRef.current = minDistance;
2601
+ distanceMaxRef.current = maxDistance;
2602
+ animateRef.current = animate;
2603
+ onChangeRef.current = onChange;
2604
+ onInteractionStartRef.current = onInteractionStart;
2605
+ onInteractionEndRef.current = onInteractionEnd;
2606
+ });
2607
+ const cameraSnapshot = () => {
2608
+ const s = cameraRef.current.state;
2609
+ return { rotX: s.rotX, rotY: s.rotY, zoom: s.zoom, target: s.target, distance: s.distance };
2610
+ };
2611
+ const fireChange = () => {
2612
+ const fn = onChangeRef.current;
2613
+ if (!fn) return;
2614
+ try {
2615
+ fn(cameraSnapshot());
2616
+ } catch (err) {
2617
+ console.error("[polycss/react] PolyOrbitControls onChange threw:", err);
2618
+ }
2619
+ };
2620
+ const fireStart = () => {
2621
+ const fn = onInteractionStartRef.current;
2622
+ if (!fn) return;
2623
+ try {
2624
+ fn(cameraSnapshot());
2625
+ } catch (err) {
2626
+ console.error("[polycss/react] PolyOrbitControls onInteractionStart threw:", err);
2627
+ }
2628
+ };
2629
+ const fireEnd = () => {
2630
+ const fn = onInteractionEndRef.current;
2631
+ if (!fn) return;
2632
+ try {
2633
+ fn(cameraSnapshot());
2634
+ } catch (err) {
2635
+ console.error("[polycss/react] PolyOrbitControls onInteractionEnd threw:", err);
2636
+ }
2637
+ };
2638
+ const animationPausedShared = useRef6({ value: false }).current;
2639
+ useEffect6(() => {
2640
+ if (!drag) return;
2641
+ const el = cameraElRef.current;
2642
+ if (!el) return;
2643
+ let activePointerId = null;
2644
+ let pointer = { x: 0, y: 0 };
2645
+ let animationPaused = false;
2646
+ let rightDragActive = false;
2647
+ let rightPointer = { x: 0, y: 0 };
2648
+ const onDown = (e) => {
2649
+ if (!dragRef.current) return;
2650
+ if (activePointerId !== null) return;
2651
+ if (e.isPrimary === false) return;
2652
+ e.preventDefault();
2653
+ activePointerId = e.pointerId;
2654
+ pointer = { x: e.clientX, y: e.clientY };
2655
+ el.style.cursor = "grabbing";
2656
+ try {
2657
+ e.target.setPointerCapture(e.pointerId);
2658
+ } catch {
2659
+ }
2660
+ const a = animateRef.current;
2661
+ if (a && a.pauseOnInteraction !== false) {
2662
+ animationPaused = true;
2663
+ animationPausedShared.value = true;
2664
+ }
2665
+ fireStart();
2666
+ };
2667
+ const onMove = (e) => {
2668
+ if (activePointerId === null || e.pointerId !== activePointerId) return;
2669
+ if (!dragRef.current) return;
2670
+ e.preventDefault();
2671
+ const dx = e.clientX - pointer.x;
2672
+ const dy = e.clientY - pointer.y;
2673
+ pointer = { x: e.clientX, y: e.clientY };
2674
+ const handle = cameraRef.current;
2675
+ if (e.shiftKey) {
2676
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2677
+ } else {
2678
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2679
+ }
2680
+ applyTransformDirect();
2681
+ store.updateCameraFromRef(handle);
2682
+ fireChange();
2683
+ };
2684
+ const onUp = (e) => {
2685
+ if (activePointerId !== e.pointerId) return;
2686
+ activePointerId = null;
2687
+ el.style.cursor = dragRef.current ? "grab" : "";
2688
+ try {
2689
+ e.target.releasePointerCapture(e.pointerId);
2690
+ } catch {
2691
+ }
2692
+ if (animationPaused) {
2693
+ animationPaused = false;
2694
+ animationPausedShared.value = false;
2695
+ }
2696
+ fireEnd();
2697
+ };
2698
+ const onContextMenu = (e) => {
2699
+ e.preventDefault();
2700
+ };
2701
+ const onMouseDown = (e) => {
2702
+ if (e.button !== 2) return;
2703
+ rightDragActive = true;
2704
+ rightPointer = { x: e.clientX, y: e.clientY };
2705
+ };
2706
+ const onMouseMove = (e) => {
2707
+ if (!rightDragActive || !dragRef.current) return;
2708
+ const dx = e.clientX - rightPointer.x;
2709
+ const dy = e.clientY - rightPointer.y;
2710
+ rightPointer = { x: e.clientX, y: e.clientY };
2711
+ const handle = cameraRef.current;
2712
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2713
+ applyTransformDirect();
2714
+ store.updateCameraFromRef(handle);
2715
+ fireChange();
2716
+ };
2717
+ const onMouseUp = (e) => {
2718
+ if (e.button !== 2) return;
2719
+ if (rightDragActive) {
2720
+ rightDragActive = false;
2721
+ fireEnd();
2722
+ }
2723
+ };
2724
+ el.style.cursor = "grab";
2725
+ el.style.touchAction = "none";
2726
+ el.style.userSelect = "none";
2727
+ el.addEventListener("pointerdown", onDown);
2728
+ el.addEventListener("pointermove", onMove);
2729
+ el.addEventListener("pointerup", onUp);
2730
+ el.addEventListener("pointercancel", onUp);
2731
+ el.addEventListener("contextmenu", onContextMenu);
2732
+ el.addEventListener("mousedown", onMouseDown);
2733
+ el.addEventListener("mousemove", onMouseMove);
2734
+ el.addEventListener("mouseup", onMouseUp);
2735
+ return () => {
2736
+ el.removeEventListener("pointerdown", onDown);
2737
+ el.removeEventListener("pointermove", onMove);
2738
+ el.removeEventListener("pointerup", onUp);
2739
+ el.removeEventListener("pointercancel", onUp);
2740
+ el.removeEventListener("contextmenu", onContextMenu);
2741
+ el.removeEventListener("mousedown", onMouseDown);
2742
+ el.removeEventListener("mousemove", onMouseMove);
2743
+ el.removeEventListener("mouseup", onMouseUp);
2744
+ el.style.cursor = "";
2745
+ el.style.touchAction = "";
2746
+ el.style.userSelect = "";
2747
+ };
2748
+ }, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
2749
+ useEffect6(
2750
+ () => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
2751
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2752
+ [wheel, applyTransformDirect, cameraElRef, cameraRef, store]
2753
+ );
2754
+ useEffect6(
2755
+ () => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
2756
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2757
+ [!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
2758
+ );
2759
+ return null;
2760
+ }
2761
+
2762
+ // src/controls/PolyMapControls.tsx
2763
+ import { useEffect as useEffect7, useRef as useRef7 } from "react";
2764
+ function PolyMapControls({
2765
+ drag = true,
2766
+ wheel = true,
2767
+ dolly = false,
2768
+ invert = false,
2769
+ minZoom = 0.1,
2770
+ maxZoom = 10,
2771
+ minDistance = 0,
2772
+ maxDistance = 5e3,
2773
+ animate = false,
2774
+ onChange,
2775
+ onInteractionStart,
2776
+ onInteractionEnd
2777
+ }) {
2778
+ const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
2779
+ const dragRef = useRef7(drag);
2780
+ const wheelRef = useRef7(wheel);
2781
+ const dollyRef = useRef7(dolly);
2782
+ const invertRef = useRef7(invert);
2783
+ const zoomMinRef = useRef7(minZoom);
2784
+ const zoomMaxRef = useRef7(maxZoom);
2785
+ const distanceMinRef = useRef7(minDistance);
2786
+ const distanceMaxRef = useRef7(maxDistance);
2787
+ const animateRef = useRef7(animate);
2788
+ const onChangeRef = useRef7(onChange);
2789
+ const onInteractionStartRef = useRef7(onInteractionStart);
2790
+ const onInteractionEndRef = useRef7(onInteractionEnd);
2791
+ useEffect7(() => {
2792
+ dragRef.current = drag;
2793
+ wheelRef.current = wheel;
2794
+ dollyRef.current = dolly;
2795
+ invertRef.current = invert;
2796
+ zoomMinRef.current = minZoom;
2797
+ zoomMaxRef.current = maxZoom;
2798
+ distanceMinRef.current = minDistance;
2799
+ distanceMaxRef.current = maxDistance;
2800
+ animateRef.current = animate;
2801
+ onChangeRef.current = onChange;
2802
+ onInteractionStartRef.current = onInteractionStart;
2803
+ onInteractionEndRef.current = onInteractionEnd;
2804
+ });
2805
+ const cameraSnapshot = () => {
2806
+ const s = cameraRef.current.state;
2807
+ return { rotX: s.rotX, rotY: s.rotY, zoom: s.zoom, target: s.target, distance: s.distance };
2808
+ };
2809
+ const fireChange = () => {
2810
+ const fn = onChangeRef.current;
2811
+ if (!fn) return;
2812
+ try {
2813
+ fn(cameraSnapshot());
2814
+ } catch (err) {
2815
+ console.error("[polycss/react] PolyMapControls onChange threw:", err);
2816
+ }
2817
+ };
2818
+ const fireStart = () => {
2819
+ const fn = onInteractionStartRef.current;
2820
+ if (!fn) return;
2821
+ try {
2822
+ fn(cameraSnapshot());
2823
+ } catch (err) {
2824
+ console.error("[polycss/react] PolyMapControls onInteractionStart threw:", err);
2825
+ }
2826
+ };
2827
+ const fireEnd = () => {
2828
+ const fn = onInteractionEndRef.current;
2829
+ if (!fn) return;
2830
+ try {
2831
+ fn(cameraSnapshot());
2832
+ } catch (err) {
2833
+ console.error("[polycss/react] PolyMapControls onInteractionEnd threw:", err);
2834
+ }
2835
+ };
2836
+ const animationPausedShared = useRef7({ value: false }).current;
2837
+ useEffect7(() => {
2838
+ if (!drag) return;
2839
+ const el = cameraElRef.current;
2840
+ if (!el) return;
2841
+ let activePointerId = null;
2842
+ let pointer = { x: 0, y: 0 };
2843
+ let animationPaused = false;
2844
+ let rightDragActive = false;
2845
+ let rightPointer = { x: 0, y: 0 };
2846
+ const onDown = (e) => {
2847
+ if (!dragRef.current) return;
2848
+ if (activePointerId !== null) return;
2849
+ if (e.isPrimary === false) return;
2850
+ e.preventDefault();
2851
+ activePointerId = e.pointerId;
2852
+ pointer = { x: e.clientX, y: e.clientY };
2853
+ el.style.cursor = "grabbing";
2854
+ try {
2855
+ e.target.setPointerCapture(e.pointerId);
2856
+ } catch {
2857
+ }
2858
+ const a = animateRef.current;
2859
+ if (a && a.pauseOnInteraction !== false) {
2860
+ animationPaused = true;
2861
+ animationPausedShared.value = true;
2862
+ }
2863
+ fireStart();
2864
+ };
2865
+ const onMove = (e) => {
2866
+ if (activePointerId === null || e.pointerId !== activePointerId) return;
2867
+ if (!dragRef.current) return;
2868
+ e.preventDefault();
2869
+ const dx = e.clientX - pointer.x;
2870
+ const dy = e.clientY - pointer.y;
2871
+ pointer = { x: e.clientX, y: e.clientY };
2872
+ const handle = cameraRef.current;
2873
+ if (e.shiftKey) {
2874
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2875
+ } else {
2876
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2877
+ }
2878
+ applyTransformDirect();
2879
+ store.updateCameraFromRef(handle);
2880
+ fireChange();
2881
+ };
2882
+ const onUp = (e) => {
2883
+ if (activePointerId !== e.pointerId) return;
2884
+ activePointerId = null;
2885
+ el.style.cursor = dragRef.current ? "grab" : "";
2886
+ try {
2887
+ e.target.releasePointerCapture(e.pointerId);
2888
+ } catch {
2889
+ }
2890
+ if (animationPaused) {
2891
+ animationPaused = false;
2892
+ animationPausedShared.value = false;
2893
+ }
2894
+ fireEnd();
2895
+ };
2896
+ const onContextMenu = (e) => {
2897
+ e.preventDefault();
2898
+ };
2899
+ const onMouseDown = (e) => {
2900
+ if (e.button !== 2) return;
2901
+ rightDragActive = true;
2902
+ rightPointer = { x: e.clientX, y: e.clientY };
2903
+ };
2904
+ const onMouseMove = (e) => {
2905
+ if (!rightDragActive || !dragRef.current) return;
2906
+ const dx = e.clientX - rightPointer.x;
2907
+ const dy = e.clientY - rightPointer.y;
2908
+ rightPointer = { x: e.clientX, y: e.clientY };
2909
+ const handle = cameraRef.current;
2910
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2911
+ applyTransformDirect();
2912
+ store.updateCameraFromRef(handle);
2913
+ fireChange();
2914
+ };
2915
+ const onMouseUp = (e) => {
2916
+ if (e.button !== 2) return;
2917
+ if (rightDragActive) {
2918
+ rightDragActive = false;
2919
+ fireEnd();
2920
+ }
2921
+ };
2922
+ el.style.cursor = "grab";
2923
+ el.style.touchAction = "none";
2924
+ el.style.userSelect = "none";
2925
+ el.addEventListener("pointerdown", onDown);
2926
+ el.addEventListener("pointermove", onMove);
2927
+ el.addEventListener("pointerup", onUp);
2928
+ el.addEventListener("pointercancel", onUp);
2929
+ el.addEventListener("contextmenu", onContextMenu);
2930
+ el.addEventListener("mousedown", onMouseDown);
2931
+ el.addEventListener("mousemove", onMouseMove);
2932
+ el.addEventListener("mouseup", onMouseUp);
2933
+ return () => {
2934
+ el.removeEventListener("pointerdown", onDown);
2935
+ el.removeEventListener("pointermove", onMove);
2936
+ el.removeEventListener("pointerup", onUp);
2937
+ el.removeEventListener("pointercancel", onUp);
2938
+ el.removeEventListener("contextmenu", onContextMenu);
2939
+ el.removeEventListener("mousedown", onMouseDown);
2940
+ el.removeEventListener("mousemove", onMouseMove);
2941
+ el.removeEventListener("mouseup", onMouseUp);
2942
+ el.style.cursor = "";
2943
+ el.style.touchAction = "";
2944
+ el.style.userSelect = "";
2945
+ };
2946
+ }, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
2947
+ useEffect7(
2948
+ () => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
2949
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2950
+ [wheel, applyTransformDirect, cameraElRef, cameraRef, store]
2951
+ );
2952
+ useEffect7(
2953
+ () => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
2954
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2955
+ [!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
2956
+ );
2957
+ return null;
2958
+ }
2959
+
2960
+ // src/controls/TransformControls.tsx
2961
+ import {
2962
+ useCallback as useCallback7,
2963
+ useContext as useContext4,
2964
+ useEffect as useEffect8,
2965
+ useMemo as useMemo10,
2966
+ useRef as useRef8,
2967
+ useState as useState4
2968
+ } from "react";
2969
+ import { arrowPolygons, ringPolygons } from "@layoutit/polycss-core";
2970
+ import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
2971
+ var COLOR_X = "#ff3653";
2972
+ var COLOR_Y = "#8adb00";
2973
+ var COLOR_Z = "#2c8fff";
2974
+ var ALPHA_IDLE = 0.6;
2975
+ var ALPHA_HOVER = 0.8;
2976
+ var ALPHA_DRAGGING = 1;
2977
+ function withAlpha(hex, alpha) {
2978
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
2979
+ if (!m) return hex;
2980
+ const r = parseInt(m[1], 16);
2981
+ const g = parseInt(m[2], 16);
2982
+ const b = parseInt(m[3], 16);
2983
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
2984
+ }
2985
+ var SCENE_TILE_SIZE = 50;
2986
+ var FALLBACK_SHAFT_LENGTH = 60;
2987
+ var SHAFT_LENGTH_RATIO = 0.6;
2988
+ var SHAFT_HALF_THICKNESS_RATIO = 0.0125;
2989
+ var HEAD_LENGTH_RATIO = 0.15;
2990
+ var HEAD_HALF_THICKNESS_RATIO = 0.04;
2991
+ var RING_RADIUS_RATIO = 1;
2992
+ var RING_HALF_THICKNESS_RATIO = 0.012;
2993
+ var RING_SEGMENTS = 64;
2994
+ var SCREEN_AXIS_DEAD_ZONE_SQ = 1e-4;
2995
+ function gizmoLengthForMesh(polygons) {
2996
+ if (polygons.length === 0) return FALLBACK_SHAFT_LENGTH;
2997
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
2998
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
2999
+ for (const poly of polygons) {
3000
+ for (const v of poly.vertices) {
3001
+ if (v[0] < minX) minX = v[0];
3002
+ if (v[0] > maxX) maxX = v[0];
3003
+ if (v[1] < minY) minY = v[1];
3004
+ if (v[1] > maxY) maxY = v[1];
3005
+ if (v[2] < minZ) minZ = v[2];
3006
+ if (v[2] > maxZ) maxZ = v[2];
3007
+ }
3008
+ }
3009
+ if (!Number.isFinite(minX)) return FALLBACK_SHAFT_LENGTH;
3010
+ const extent = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
3011
+ return extent * SCENE_TILE_SIZE * SHAFT_LENGTH_RATIO;
3012
+ }
3013
+ function gizmoCenterForMesh(polygons) {
3014
+ if (polygons.length === 0) return [0, 0, 0];
3015
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
3016
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
3017
+ for (const poly of polygons) {
3018
+ for (const v of poly.vertices) {
3019
+ if (v[0] < minX) minX = v[0];
3020
+ if (v[0] > maxX) maxX = v[0];
3021
+ if (v[1] < minY) minY = v[1];
3022
+ if (v[1] > maxY) maxY = v[1];
3023
+ if (v[2] < minZ) minZ = v[2];
3024
+ if (v[2] > maxZ) maxZ = v[2];
3025
+ }
3026
+ }
3027
+ if (!Number.isFinite(minX)) return [0, 0, 0];
3028
+ return [
3029
+ (minY + maxY) / 2 * SCENE_TILE_SIZE,
3030
+ (minX + maxX) / 2 * SCENE_TILE_SIZE,
3031
+ (minZ + maxZ) / 2 * SCENE_TILE_SIZE
3032
+ ];
3033
+ }
3034
+ function resolveObject(o) {
3035
+ if (o == null) return null;
3036
+ if (typeof o === "object" && "current" in o) return o.current ?? null;
3037
+ return o;
3038
+ }
3039
+ function snap(value, step) {
3040
+ if (!step || step <= 0) return value;
3041
+ return Math.round(value / step) * step;
3042
+ }
3043
+ var WORLD_AXIS_FOR_CSS = { 0: 1, 1: 0, 2: 2 };
3044
+ var ARROW_SPECS = [
3045
+ { cssAxis: 0, sign: 1, key: "x", color: COLOR_X },
3046
+ { cssAxis: 0, sign: -1, key: "-x", color: COLOR_X },
3047
+ { cssAxis: 1, sign: 1, key: "y", color: COLOR_Y },
3048
+ { cssAxis: 1, sign: -1, key: "-y", color: COLOR_Y },
3049
+ { cssAxis: 2, sign: 1, key: "z", color: COLOR_Z },
3050
+ { cssAxis: 2, sign: -1, key: "-z", color: COLOR_Z }
3051
+ ];
3052
+ var RING_SPECS = [
3053
+ { cssAxis: 0, key: "x", color: COLOR_X },
3054
+ { cssAxis: 1, key: "y", color: COLOR_Y },
3055
+ { cssAxis: 2, key: "z", color: COLOR_Z }
3056
+ ];
3057
+ function userAxisLetterOf(key) {
3058
+ const last = key.replace("-", "")[0];
3059
+ return last;
3060
+ }
3061
+ function startAxisDrag(opts) {
3062
+ const {
3063
+ cssAxis,
3064
+ sign,
3065
+ shaftLengthCss,
3066
+ wrapper,
3067
+ target,
3068
+ startClientX,
3069
+ startClientY,
3070
+ translationSnap,
3071
+ onChange,
3072
+ onObjectChange,
3073
+ onMouseDown,
3074
+ onMouseUp,
3075
+ onDraggingChanged
3076
+ } = opts;
3077
+ const probeDistance = shaftLengthCss;
3078
+ const axisVec = [0, 0, 0];
3079
+ axisVec[cssAxis] = sign;
3080
+ const probe = document.createElement("div");
3081
+ probe.style.position = "absolute";
3082
+ probe.style.left = "0";
3083
+ probe.style.top = "0";
3084
+ probe.style.width = "0";
3085
+ probe.style.height = "0";
3086
+ probe.style.transform = `translate3d(${axisVec[0] * probeDistance}px, ${axisVec[1] * probeDistance}px, ${axisVec[2] * probeDistance}px)`;
3087
+ wrapper.appendChild(probe);
3088
+ const wRect = wrapper.getBoundingClientRect();
3089
+ const pRect = probe.getBoundingClientRect();
3090
+ wrapper.removeChild(probe);
3091
+ const screenAxisX = (pRect.left - wRect.left) / probeDistance;
3092
+ const screenAxisY = (pRect.top - wRect.top) / probeDistance;
3093
+ const screenAxisLenSq = screenAxisX * screenAxisX + screenAxisY * screenAxisY;
3094
+ if (screenAxisLenSq < SCREEN_AXIS_DEAD_ZONE_SQ) return;
3095
+ const startPos = target.getPosition() ?? [0, 0, 0];
3096
+ onMouseDown?.();
3097
+ onDraggingChanged?.(true);
3098
+ const handleMove = (ev) => {
3099
+ const dx = ev.clientX - startClientX;
3100
+ const dy = ev.clientY - startClientY;
3101
+ let t = (dx * screenAxisX + dy * screenAxisY) / screenAxisLenSq;
3102
+ t = snap(t, translationSnap);
3103
+ const newPos = [
3104
+ startPos[0] + t * axisVec[0],
3105
+ startPos[1] + t * axisVec[1],
3106
+ startPos[2] + t * axisVec[2]
3107
+ ];
3108
+ onObjectChange?.({ object: target, position: newPos });
3109
+ onChange?.();
3110
+ };
3111
+ const handleUp = () => {
3112
+ window.removeEventListener("pointermove", handleMove);
3113
+ window.removeEventListener("pointerup", handleUp);
3114
+ window.removeEventListener("pointercancel", handleUp);
3115
+ const swallow = (e) => {
3116
+ e.stopPropagation();
3117
+ e.stopImmediatePropagation();
3118
+ };
3119
+ window.addEventListener("click", swallow, { capture: true, once: true });
3120
+ setTimeout(() => window.removeEventListener("click", swallow, true), 0);
3121
+ onMouseUp?.();
3122
+ onDraggingChanged?.(false);
3123
+ };
3124
+ window.addEventListener("pointermove", handleMove);
3125
+ window.addEventListener("pointerup", handleUp);
3126
+ window.addEventListener("pointercancel", handleUp);
3127
+ }
3128
+ function startRingDrag(opts) {
3129
+ const {
3130
+ cssAxis,
3131
+ wrapper,
3132
+ target,
3133
+ startClientX,
3134
+ startClientY,
3135
+ rotationSnap,
3136
+ onChange,
3137
+ onObjectChange,
3138
+ onMouseDown,
3139
+ onMouseUp,
3140
+ onDraggingChanged
3141
+ } = opts;
3142
+ const wRect = wrapper.getBoundingClientRect();
3143
+ const centerX = wRect.left;
3144
+ const centerY = wRect.top;
3145
+ let lastAngle = Math.atan2(startClientY - centerY, startClientX - centerX);
3146
+ let cumulative = 0;
3147
+ const startRotation = target.getRotation() ?? [0, 0, 0];
3148
+ onMouseDown?.();
3149
+ onDraggingChanged?.(true);
3150
+ const handleMove = (ev) => {
3151
+ const a = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
3152
+ let d = a - lastAngle;
3153
+ if (d > Math.PI) d -= 2 * Math.PI;
3154
+ else if (d < -Math.PI) d += 2 * Math.PI;
3155
+ cumulative += d;
3156
+ lastAngle = a;
3157
+ let degrees = cumulative * 180 / Math.PI;
3158
+ degrees = snap(degrees, rotationSnap);
3159
+ const newRotation = [
3160
+ startRotation[0],
3161
+ startRotation[1],
3162
+ startRotation[2]
3163
+ ];
3164
+ newRotation[cssAxis] = startRotation[cssAxis] + degrees;
3165
+ onObjectChange?.({ object: target, rotation: newRotation });
3166
+ onChange?.();
3167
+ };
3168
+ const handleUp = () => {
3169
+ window.removeEventListener("pointermove", handleMove);
3170
+ window.removeEventListener("pointerup", handleUp);
3171
+ window.removeEventListener("pointercancel", handleUp);
3172
+ const swallow = (e) => {
3173
+ e.stopPropagation();
3174
+ e.stopImmediatePropagation();
3175
+ };
3176
+ window.addEventListener("click", swallow, { capture: true, once: true });
3177
+ setTimeout(() => window.removeEventListener("click", swallow, true), 0);
3178
+ onMouseUp?.();
3179
+ onDraggingChanged?.(false);
3180
+ target.rebakeAtlas();
3181
+ };
3182
+ window.addEventListener("pointermove", handleMove);
3183
+ window.addEventListener("pointerup", handleUp);
3184
+ window.addEventListener("pointercancel", handleUp);
3185
+ }
3186
+ function PolyTransformControls({
3187
+ object,
3188
+ mode = "translate",
3189
+ space = "world",
3190
+ size = 1,
3191
+ showX = true,
3192
+ showY = true,
3193
+ showZ = true,
3194
+ translationSnap = null,
3195
+ rotationSnap = null,
3196
+ enabled = true,
3197
+ onChange,
3198
+ onObjectChange,
3199
+ onMouseDown,
3200
+ onMouseUp,
3201
+ onDraggingChanged
3202
+ }) {
3203
+ const [, forceRender] = useState4(0);
3204
+ useEffect8(() => {
3205
+ forceRender((n) => n + 1);
3206
+ }, [object]);
3207
+ const [hoveredKey, setHoveredKey] = useState4(null);
3208
+ const [draggingKey, setDraggingKey] = useState4(null);
3209
+ const cameraCtx = useContext4(PolyCameraContext);
3210
+ const cameraElRef = cameraCtx?.cameraElRef;
3211
+ const dragRef = useRef8({
3212
+ target: null,
3213
+ mode: "translate",
3214
+ shaftLengthCss: 0,
3215
+ enabled: true,
3216
+ show: { x: true, y: true, z: true },
3217
+ translationSnap: null,
3218
+ rotationSnap: null
3219
+ });
3220
+ useEffect8(() => {
3221
+ const cameraEl = cameraElRef?.current;
3222
+ if (!cameraEl) return;
3223
+ const onPointerDown = (event) => {
3224
+ const state = dragRef.current;
3225
+ if (!state.target || !state.enabled) return;
3226
+ const targetEl = event.target;
3227
+ if (targetEl?.closest(".polycss-transform-gizmo")) return;
3228
+ if (state.mode === "translate") {
3229
+ for (const spec of ARROW_SPECS) {
3230
+ if (!state.show[userAxisLetterOf(spec.key)]) continue;
3231
+ const arrowEl = document.querySelector(
3232
+ `.polycss-transform-arrow--${spec.key}`
3233
+ );
3234
+ if (!arrowEl) continue;
3235
+ if (!pointInMeshElement(arrowEl, event.clientX, event.clientY)) continue;
3236
+ event.preventDefault();
3237
+ event.stopPropagation();
3238
+ const wrapper = arrowEl.closest(
3239
+ "[data-poly-transform-controls]"
3240
+ );
3241
+ if (!wrapper) return;
3242
+ setDraggingKey(spec.key);
3243
+ startAxisDrag({
3244
+ cssAxis: spec.cssAxis,
3245
+ sign: spec.sign,
3246
+ shaftLengthCss: state.shaftLengthCss,
3247
+ wrapper,
3248
+ target: state.target,
3249
+ startClientX: event.clientX,
3250
+ startClientY: event.clientY,
3251
+ translationSnap: state.translationSnap,
3252
+ onChange: state.onChange,
3253
+ onObjectChange: state.onObjectChange,
3254
+ onMouseDown: state.onMouseDown,
3255
+ onMouseUp: state.onMouseUp,
3256
+ onDraggingChanged: (d) => {
3257
+ if (!d) setDraggingKey(null);
3258
+ state.onDraggingChanged?.(d);
3259
+ }
3260
+ });
3261
+ return;
3262
+ }
3263
+ } else if (state.mode === "rotate") {
3264
+ for (const spec of RING_SPECS) {
3265
+ if (!state.show[spec.key]) continue;
3266
+ const ringEl = document.querySelector(
3267
+ `.polycss-transform-ring--${spec.key}`
3268
+ );
3269
+ if (!ringEl) continue;
3270
+ if (!pointInMeshElement(ringEl, event.clientX, event.clientY)) continue;
3271
+ event.preventDefault();
3272
+ event.stopPropagation();
3273
+ const wrapper = ringEl.closest(
3274
+ "[data-poly-transform-controls]"
3275
+ );
3276
+ if (!wrapper) return;
3277
+ setDraggingKey(spec.key);
3278
+ startRingDrag({
3279
+ cssAxis: spec.cssAxis,
3280
+ wrapper,
3281
+ target: state.target,
3282
+ startClientX: event.clientX,
3283
+ startClientY: event.clientY,
3284
+ rotationSnap: state.rotationSnap,
3285
+ onChange: state.onChange,
3286
+ onObjectChange: state.onObjectChange,
3287
+ onMouseDown: state.onMouseDown,
3288
+ onMouseUp: state.onMouseUp,
3289
+ onDraggingChanged: (d) => {
3290
+ if (!d) setDraggingKey(null);
3291
+ state.onDraggingChanged?.(d);
3292
+ }
3293
+ });
3294
+ return;
3295
+ }
3296
+ }
3297
+ };
3298
+ cameraEl.addEventListener("pointerdown", onPointerDown);
3299
+ return () => cameraEl.removeEventListener("pointerdown", onPointerDown);
3300
+ }, [cameraElRef]);
3301
+ const target = resolveObject(object);
3302
+ if (!target) return null;
3303
+ const position = target.getPosition() ?? [0, 0, 0];
3304
+ const polygons = target.getPolygons();
3305
+ const bboxCenter = gizmoCenterForMesh(polygons);
3306
+ const wrapperPos = [
3307
+ position[0] + bboxCenter[0],
3308
+ position[1] + bboxCenter[1],
3309
+ position[2] + bboxCenter[2]
3310
+ ];
3311
+ const baseLength = gizmoLengthForMesh(polygons);
3312
+ const shaftLengthCss = baseLength * size;
3313
+ dragRef.current = {
3314
+ target,
3315
+ mode,
3316
+ shaftLengthCss,
3317
+ enabled,
3318
+ show: { x: showX, y: showY, z: showZ },
3319
+ translationSnap,
3320
+ rotationSnap,
3321
+ onChange,
3322
+ onObjectChange,
3323
+ onMouseDown,
3324
+ onMouseUp,
3325
+ onDraggingChanged
3326
+ };
3327
+ const wrapperStyle = {
3328
+ position: "absolute",
3329
+ transformStyle: "preserve-3d",
3330
+ transform: `translate3d(${wrapperPos[0]}px, ${wrapperPos[1]}px, ${wrapperPos[2]}px)`,
3331
+ // No `pointer-events: none` here — that property is inherited, so
3332
+ // setting it on the wrapper would cascade to every arrow polygon
3333
+ // and disable native hit-testing on the gizmo entirely. The
3334
+ // wrapper is a 0×0 anchor so it has no surface to be hit on its
3335
+ // own; descendants opt in via the default `auto`.
3336
+ zIndex: 1e3
3337
+ };
3338
+ return /* @__PURE__ */ jsxs2(
3339
+ "div",
3340
+ {
3341
+ className: "polycss-transform-controls",
3342
+ "data-poly-transform-controls": true,
3343
+ "data-poly-mode": mode,
3344
+ "data-poly-space": space,
3345
+ style: wrapperStyle,
3346
+ children: [
3347
+ mode === "translate" && ARROW_SPECS.map((spec) => {
3348
+ const show = { x: showX, y: showY, z: showZ }[userAxisLetterOf(spec.key)];
3349
+ if (!show) return null;
3350
+ const hovered = hoveredKey === spec.key;
3351
+ const dragging = draggingKey === spec.key;
3352
+ const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
3353
+ return /* @__PURE__ */ jsx7(
3354
+ TranslateArrow,
3355
+ {
3356
+ cssAxis: spec.cssAxis,
3357
+ sign: spec.sign,
3358
+ axisKey: spec.key,
3359
+ color: withAlpha(spec.color, alpha),
3360
+ shaftLengthCss,
3361
+ target,
3362
+ enabled,
3363
+ translationSnap,
3364
+ onChange,
3365
+ onObjectChange,
3366
+ onMouseDown,
3367
+ onMouseUp,
3368
+ onDraggingChanged,
3369
+ onHoverChange: (h) => setHoveredKey(h ? spec.key : (cur) => cur === spec.key ? null : cur),
3370
+ onDraggingStart: () => setDraggingKey(spec.key),
3371
+ onDraggingStop: () => setDraggingKey((cur) => cur === spec.key ? null : cur)
3372
+ },
3373
+ spec.key
3374
+ );
3375
+ }),
3376
+ mode === "rotate" && RING_SPECS.map((spec) => {
3377
+ const show = { x: showX, y: showY, z: showZ }[spec.key];
3378
+ if (!show) return null;
3379
+ const hovered = hoveredKey === spec.key;
3380
+ const dragging = draggingKey === spec.key;
3381
+ const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
3382
+ return /* @__PURE__ */ jsx7(
3383
+ RotateRing,
3384
+ {
3385
+ cssAxis: spec.cssAxis,
3386
+ axisKey: spec.key,
3387
+ color: withAlpha(spec.color, alpha),
3388
+ radiusCss: shaftLengthCss * RING_RADIUS_RATIO,
3389
+ target,
3390
+ enabled,
3391
+ rotationSnap,
3392
+ onChange,
3393
+ onObjectChange,
3394
+ onMouseDown,
3395
+ onMouseUp,
3396
+ onDraggingChanged,
3397
+ onHoverChange: (h) => setHoveredKey(h ? spec.key : (cur) => cur === spec.key ? null : cur),
3398
+ onDraggingStart: () => setDraggingKey(spec.key),
3399
+ onDraggingStop: () => setDraggingKey((cur) => cur === spec.key ? null : cur)
3400
+ },
3401
+ spec.key
3402
+ );
3403
+ })
3404
+ ]
3405
+ }
3406
+ );
3407
+ }
3408
+ function TranslateArrow({
3409
+ cssAxis,
3410
+ sign,
3411
+ axisKey,
3412
+ color,
3413
+ shaftLengthCss,
3414
+ target,
3415
+ enabled,
3416
+ translationSnap,
3417
+ onChange,
3418
+ onObjectChange,
3419
+ onMouseDown,
3420
+ onMouseUp,
3421
+ onDraggingChanged,
3422
+ onHoverChange,
3423
+ onDraggingStart,
3424
+ onDraggingStop
3425
+ }) {
3426
+ const cbRef = useRef8({
3427
+ onChange,
3428
+ onObjectChange,
3429
+ onMouseDown,
3430
+ onMouseUp,
3431
+ onDraggingChanged,
3432
+ onDraggingStart,
3433
+ onDraggingStop,
3434
+ enabled,
3435
+ translationSnap
3436
+ });
3437
+ cbRef.current = {
3438
+ onChange,
3439
+ onObjectChange,
3440
+ onMouseDown,
3441
+ onMouseUp,
3442
+ onDraggingChanged,
3443
+ onDraggingStart,
3444
+ onDraggingStop,
3445
+ enabled,
3446
+ translationSnap
3447
+ };
3448
+ const polygons = useMemo10(() => {
3449
+ const lengthWorld = shaftLengthCss / SCENE_TILE_SIZE;
3450
+ return arrowPolygons({
3451
+ axis: WORLD_AXIS_FOR_CSS[cssAxis],
3452
+ sign,
3453
+ shaftLength: lengthWorld,
3454
+ shaftHalfThickness: lengthWorld * SHAFT_HALF_THICKNESS_RATIO,
3455
+ headLength: lengthWorld * HEAD_LENGTH_RATIO,
3456
+ headHalfThickness: lengthWorld * HEAD_HALF_THICKNESS_RATIO,
3457
+ color
3458
+ });
3459
+ }, [cssAxis, sign, color, shaftLengthCss]);
3460
+ const onPointerDown = useCallback7(
3461
+ (e) => {
3462
+ if (!cbRef.current.enabled) return;
3463
+ e.stopPropagation();
3464
+ const meshEl = e.eventObject.element;
3465
+ const wrapper = meshEl?.closest("[data-poly-transform-controls]");
3466
+ if (!wrapper) return;
3467
+ cbRef.current.onDraggingStart?.();
3468
+ startAxisDrag({
3469
+ cssAxis,
3470
+ sign,
3471
+ shaftLengthCss,
3472
+ wrapper,
3473
+ target,
3474
+ startClientX: e.nativeEvent.clientX,
3475
+ startClientY: e.nativeEvent.clientY,
3476
+ translationSnap: cbRef.current.translationSnap,
3477
+ onChange: cbRef.current.onChange,
3478
+ onObjectChange: cbRef.current.onObjectChange,
3479
+ onMouseDown: cbRef.current.onMouseDown,
3480
+ onMouseUp: cbRef.current.onMouseUp,
3481
+ onDraggingChanged: (d) => {
3482
+ if (!d) cbRef.current.onDraggingStop?.();
3483
+ cbRef.current.onDraggingChanged?.(d);
3484
+ }
3485
+ });
3486
+ },
3487
+ [cssAxis, sign, target, shaftLengthCss]
3488
+ );
3489
+ return /* @__PURE__ */ jsx7(
3490
+ PolyMesh,
3491
+ {
3492
+ polygons,
3493
+ onPointerDown,
3494
+ onPointerOver: () => onHoverChange?.(true),
3495
+ onPointerOut: () => onHoverChange?.(false),
3496
+ className: `polycss-transform-gizmo polycss-transform-arrow polycss-transform-arrow--${axisKey}`,
3497
+ textureLighting: "baked"
3498
+ }
3499
+ );
3500
+ }
3501
+ function RotateRing({
3502
+ cssAxis,
3503
+ axisKey,
3504
+ color,
3505
+ radiusCss,
3506
+ target,
3507
+ enabled,
3508
+ rotationSnap,
3509
+ onChange,
3510
+ onObjectChange,
3511
+ onMouseDown,
3512
+ onMouseUp,
3513
+ onDraggingChanged,
3514
+ onHoverChange,
3515
+ onDraggingStart,
3516
+ onDraggingStop
3517
+ }) {
3518
+ const cbRef = useRef8({
3519
+ onChange,
3520
+ onObjectChange,
3521
+ onMouseDown,
3522
+ onMouseUp,
3523
+ onDraggingChanged,
3524
+ onDraggingStart,
3525
+ onDraggingStop,
3526
+ enabled,
3527
+ rotationSnap
3528
+ });
3529
+ cbRef.current = {
3530
+ onChange,
3531
+ onObjectChange,
3532
+ onMouseDown,
3533
+ onMouseUp,
3534
+ onDraggingChanged,
3535
+ onDraggingStart,
3536
+ onDraggingStop,
3537
+ enabled,
3538
+ rotationSnap
3539
+ };
3540
+ const polygons = useMemo10(() => {
3541
+ const radiusWorld = radiusCss / SCENE_TILE_SIZE;
3542
+ return ringPolygons({
3543
+ axis: WORLD_AXIS_FOR_CSS[cssAxis],
3544
+ radius: radiusWorld,
3545
+ halfThickness: radiusWorld * RING_HALF_THICKNESS_RATIO,
3546
+ segments: RING_SEGMENTS,
3547
+ color
3548
+ });
3549
+ }, [cssAxis, color, radiusCss]);
3550
+ const onPointerDown = useCallback7(
3551
+ (e) => {
3552
+ if (!cbRef.current.enabled) return;
3553
+ e.stopPropagation();
3554
+ const meshEl = e.eventObject.element;
3555
+ const wrapper = meshEl?.closest("[data-poly-transform-controls]");
3556
+ if (!wrapper) return;
3557
+ cbRef.current.onDraggingStart?.();
3558
+ startRingDrag({
3559
+ cssAxis,
3560
+ wrapper,
3561
+ target,
3562
+ startClientX: e.nativeEvent.clientX,
3563
+ startClientY: e.nativeEvent.clientY,
3564
+ rotationSnap: cbRef.current.rotationSnap,
3565
+ onChange: cbRef.current.onChange,
3566
+ onObjectChange: cbRef.current.onObjectChange,
3567
+ onMouseDown: cbRef.current.onMouseDown,
3568
+ onMouseUp: cbRef.current.onMouseUp,
3569
+ onDraggingChanged: (d) => {
3570
+ if (!d) cbRef.current.onDraggingStop?.();
3571
+ cbRef.current.onDraggingChanged?.(d);
3572
+ }
3573
+ });
3574
+ },
3575
+ [cssAxis, target]
3576
+ );
3577
+ return /* @__PURE__ */ jsx7(
3578
+ PolyMesh,
3579
+ {
3580
+ polygons,
3581
+ onPointerDown,
3582
+ onPointerOver: () => onHoverChange?.(true),
3583
+ onPointerOut: () => onHoverChange?.(false),
3584
+ className: `polycss-transform-gizmo polycss-transform-ring polycss-transform-ring--${axisKey}`,
3585
+ textureLighting: "baked"
3586
+ }
3587
+ );
3588
+ }
3589
+
3590
+ // src/select/Select.tsx
3591
+ import {
3592
+ createContext as createContext3,
3593
+ useCallback as useCallback8,
3594
+ useContext as useContext5,
3595
+ useEffect as useEffect9,
3596
+ useMemo as useMemo11,
3597
+ useRef as useRef9,
3598
+ useState as useState5
3599
+ } from "react";
3600
+ import { jsx as jsx8 } from "react/jsx-runtime";
3601
+ var SelectContext = createContext3(null);
3602
+ function PolySelect({
3603
+ multiple = false,
3604
+ filter,
3605
+ onChange,
3606
+ onPointerMissed,
3607
+ clearOnMiss = true,
3608
+ children,
3609
+ className,
3610
+ style
3611
+ }) {
3612
+ const [selected, setSelectedState] = useState5([]);
3613
+ const filterRef = useRef9(filter);
3614
+ filterRef.current = filter;
3615
+ const onChangeRef = useRef9(onChange);
3616
+ onChangeRef.current = onChange;
3617
+ const multipleRef = useRef9(multiple);
3618
+ multipleRef.current = multiple;
3619
+ const apply = useCallback8((next) => {
3620
+ const filtered = filterRef.current ? filterRef.current(next) : next;
3621
+ setSelectedState(filtered);
3622
+ if (onChangeRef.current) onChangeRef.current(filtered);
3623
+ }, []);
3624
+ const api = useMemo11(() => {
3625
+ return {
3626
+ selected,
3627
+ set: (next) => apply(next),
3628
+ add: (h) => {
3629
+ setSelectedState((prev) => {
3630
+ const next = multipleRef.current ? prev.includes(h) ? prev : [...prev, h] : [h];
3631
+ const filtered = filterRef.current ? filterRef.current(next) : next;
3632
+ if (onChangeRef.current) onChangeRef.current(filtered);
3633
+ return filtered;
3634
+ });
3635
+ },
3636
+ remove: (h) => {
3637
+ setSelectedState((prev) => {
3638
+ if (!prev.includes(h)) return prev;
3639
+ const next = prev.filter((x) => x !== h);
3640
+ const filtered = filterRef.current ? filterRef.current(next) : next;
3641
+ if (onChangeRef.current) onChangeRef.current(filtered);
3642
+ return filtered;
3643
+ });
3644
+ },
3645
+ toggle: (h) => {
3646
+ setSelectedState((prev) => {
3647
+ const next = prev.includes(h) ? prev.filter((x) => x !== h) : multipleRef.current ? [...prev, h] : [h];
3648
+ const filtered = filterRef.current ? filterRef.current(next) : next;
3649
+ if (onChangeRef.current) onChangeRef.current(filtered);
3650
+ return filtered;
3651
+ });
3652
+ },
3653
+ clear: () => apply([]),
3654
+ has: (h) => selected.includes(h)
3655
+ };
3656
+ }, [selected, apply]);
3657
+ const wrapperRef = useRef9(null);
3658
+ const cameraCtx = useContext5(PolyCameraContext);
3659
+ const cameraElRef = cameraCtx?.cameraElRef;
3660
+ const clearOnMissRef = useRef9(clearOnMiss);
3661
+ clearOnMissRef.current = clearOnMiss;
3662
+ const onPointerMissedRef = useRef9(onPointerMissed);
3663
+ onPointerMissedRef.current = onPointerMissed;
3664
+ const findMeshUnderPoint2 = useCallback8(
3665
+ (clientX, clientY) => findMeshUnderPoint(
3666
+ clientX,
3667
+ clientY,
3668
+ (meshEl) => !meshEl.closest("[data-poly-transform-controls]")
3669
+ ),
3670
+ []
3671
+ );
3672
+ useEffect9(() => {
3673
+ const cameraEl = cameraElRef?.current;
3674
+ if (!cameraEl) return;
3675
+ const onClick = (event) => {
3676
+ const target = event.target;
3677
+ if (target?.closest?.("[data-poly-transform-controls]")) return;
3678
+ const handle = findPolyMeshHandle(event.target) ?? findMeshUnderPoint2(event.clientX, event.clientY);
3679
+ if (!handle) {
3680
+ if (onPointerMissedRef.current) onPointerMissedRef.current(event);
3681
+ if (clearOnMissRef.current) apply([]);
3682
+ return;
3683
+ }
3684
+ const additive = multipleRef.current && (event.shiftKey || event.metaKey || event.ctrlKey);
3685
+ setSelectedState((prev) => {
3686
+ let next;
3687
+ if (additive) {
3688
+ next = prev.includes(handle) ? prev.filter((x) => x !== handle) : [...prev, handle];
3689
+ } else if (prev.length === 1 && prev[0] === handle) {
3690
+ next = [];
3691
+ } else {
3692
+ next = [handle];
3693
+ }
3694
+ const filtered = filterRef.current ? filterRef.current(next) : next;
3695
+ if (onChangeRef.current) onChangeRef.current(filtered);
3696
+ return filtered;
3697
+ });
3698
+ };
3699
+ cameraEl.addEventListener("click", onClick);
3700
+ return () => cameraEl.removeEventListener("click", onClick);
3701
+ }, [cameraElRef, findMeshUnderPoint2, apply]);
3702
+ const handleClick = (e) => {
3703
+ if (cameraCtx) return;
3704
+ const handle = findPolyMeshHandle(e.target) ?? findMeshUnderPoint2(e.clientX, e.clientY);
3705
+ if (!handle) {
3706
+ if (onPointerMissed) onPointerMissed(e);
3707
+ if (clearOnMiss) apply([]);
3708
+ return;
3709
+ }
3710
+ if (multiple && (e.shiftKey || e.metaKey || e.ctrlKey)) {
3711
+ api.toggle(handle);
3712
+ } else if (selected.length === 1 && selected[0] === handle) {
3713
+ apply([]);
3714
+ } else {
3715
+ apply([handle]);
3716
+ }
3717
+ };
3718
+ const wrapperStyle = { display: "contents", ...style };
3719
+ return /* @__PURE__ */ jsx8(SelectContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(
3720
+ "div",
3721
+ {
3722
+ ref: wrapperRef,
3723
+ className,
3724
+ style: wrapperStyle,
3725
+ onClick: handleClick,
3726
+ "data-poly-select": true,
3727
+ children
3728
+ }
3729
+ ) });
3730
+ }
3731
+ function usePolySelect() {
3732
+ return useContext5(SelectContext)?.selected ?? [];
3733
+ }
3734
+ function usePolySelectionApi() {
3735
+ const ctx = useContext5(SelectContext);
3736
+ if (!ctx) {
3737
+ throw new Error("polycss: usePolySelectionApi must be used inside <PolySelect>.");
3738
+ }
3739
+ return ctx;
3740
+ }
3741
+
3742
+ // src/helpers/PolyAxesHelper.tsx
3743
+ import { useMemo as useMemo12 } from "react";
3744
+ import { axesHelperPolygons } from "@layoutit/polycss-core";
3745
+ import { jsx as jsx9 } from "react/jsx-runtime";
3746
+ function PolyAxesHelper({
3747
+ size,
3748
+ thickness,
3749
+ negative,
3750
+ xColor,
3751
+ yColor,
3752
+ zColor
3753
+ }) {
3754
+ const polygons = useMemo12(
3755
+ () => axesHelperPolygons({ size, thickness, negative, xColor, yColor, zColor }),
3756
+ [size, thickness, negative, xColor, yColor, zColor]
3757
+ );
3758
+ return /* @__PURE__ */ jsx9(PolyMesh, { polygons });
3759
+ }
3760
+
3761
+ // src/helpers/PolyDirectionalLightHelper.tsx
3762
+ import { useMemo as useMemo13 } from "react";
3763
+ import { octahedronPolygons } from "@layoutit/polycss-core";
3764
+ import { jsx as jsx10 } from "react/jsx-runtime";
3765
+ var TILE = 50;
3766
+ function PolyDirectionalLightHelper({
3767
+ light,
3768
+ target,
3769
+ distance = 5,
3770
+ size = 0.35,
3771
+ color
3772
+ }) {
3773
+ const swatch = color ?? light.color ?? "#ffd54a";
3774
+ const polygons = useMemo13(
3775
+ () => octahedronPolygons({ center: [0, 0, 0], size, color: swatch }),
3776
+ [size, swatch]
3777
+ );
3778
+ const meshPosition = useMemo13(() => {
3779
+ const [dx, dy, dz] = light.direction;
3780
+ const len = Math.hypot(dx, dy, dz) || 1;
3781
+ const tx = target?.[0] ?? 0;
3782
+ const ty = target?.[1] ?? 0;
3783
+ const tz = target?.[2] ?? 0;
3784
+ const worldX = tx + dy / len * distance;
3785
+ const worldY = ty + dx / len * distance;
3786
+ const worldZ = tz + dz / len * distance;
3787
+ return [worldY * TILE, worldX * TILE, worldZ * TILE];
3788
+ }, [light.direction, target, distance]);
3789
+ return /* @__PURE__ */ jsx10(PolyMesh, { polygons, position: meshPosition });
3790
+ }
3791
+
3792
+ // src/styles/colorResolver.ts
3793
+ import { parsePureColor as parsePureColor2 } from "@layoutit/polycss-core";
3794
+
3795
+ // src/animation/usePolyAnimation.ts
3796
+ import { useEffect as useEffect10, useRef as useRef10, useMemo as useMemo14 } from "react";
3797
+ import { createPolyAnimationMixer } from "@layoutit/polycss-core";
3798
+ function resolveRoot(rootArg) {
3799
+ if (!rootArg) return null;
3800
+ if ("current" in rootArg) return rootArg.current;
3801
+ return rootArg;
3802
+ }
3803
+ function usePolyAnimation(clips, controller, root) {
3804
+ const internalRef = useRef10(null);
3805
+ const mixerRef = useRef10(null);
3806
+ useEffect10(() => {
3807
+ if (!clips || clips.length === 0 || !controller) {
3808
+ mixerRef.current = null;
3809
+ return;
3810
+ }
3811
+ const resolvedRoot = resolveRoot(root) ?? internalRef.current;
3812
+ if (!resolvedRoot) {
3813
+ mixerRef.current = null;
3814
+ return;
3815
+ }
3816
+ mixerRef.current = createPolyAnimationMixer(resolvedRoot, controller);
3817
+ return () => {
3818
+ mixerRef.current?.stopAllAction();
3819
+ mixerRef.current?.uncacheRoot();
3820
+ mixerRef.current = null;
3821
+ };
3822
+ }, [clips, controller]);
3823
+ useEffect10(() => {
3824
+ if (!clips || clips.length === 0 || !controller) return;
3825
+ let rafId;
3826
+ let lastTime = null;
3827
+ function tick(now) {
3828
+ if (lastTime === null) {
3829
+ lastTime = now;
3830
+ rafId = requestAnimationFrame(tick);
3831
+ return;
3832
+ }
3833
+ const dt = (now - lastTime) / 1e3;
3834
+ lastTime = now;
3835
+ mixerRef.current?.update(dt);
3836
+ rafId = requestAnimationFrame(tick);
3837
+ }
3838
+ rafId = requestAnimationFrame(tick);
3839
+ return () => {
3840
+ cancelAnimationFrame(rafId);
3841
+ lastTime = null;
3842
+ };
3843
+ }, [clips, controller]);
3844
+ const resolvedClips = clips ?? [];
3845
+ const resolvedNames = resolvedClips.map((c) => c.name);
3846
+ const actions = useMemo14(() => {
3847
+ const target = {};
3848
+ for (const clip of resolvedClips) {
3849
+ Object.defineProperty(target, clip.name, {
3850
+ enumerable: true,
3851
+ get() {
3852
+ const m = mixerRef.current;
3853
+ if (!m) return null;
3854
+ try {
3855
+ return m.clipAction(clip.name);
3856
+ } catch {
3857
+ return null;
3858
+ }
3859
+ }
3860
+ });
3861
+ }
3862
+ return target;
3863
+ }, [resolvedClips]);
3864
+ return {
3865
+ ref: internalRef,
3866
+ mixer: mixerRef.current,
3867
+ clips: resolvedClips,
3868
+ names: resolvedNames,
3869
+ actions
3870
+ };
3871
+ }
3872
+
3873
+ // src/index.ts
3874
+ import {
3875
+ normalizePolygons,
3876
+ mergePolygons as mergePolygons2,
3877
+ coverPlanarPolygons,
3878
+ cullInteriorPolygons,
3879
+ parseObj,
3880
+ parseMtl,
3881
+ parseGltf,
3882
+ bakeSolidTextureSamples,
3883
+ bakeSolidTextureSampledPolygons,
3884
+ loadMesh as loadMesh2,
3885
+ createIsometricCamera as createIsometricCamera3,
3886
+ parseVox,
3887
+ polygonFaces,
3888
+ computeTexturePaintMetrics,
3889
+ computeShapeLighting,
3890
+ parseColor,
3891
+ parsePureColor as parsePureColor3,
3892
+ parseHexColor as parseHexColor2,
3893
+ parseRgbColor,
3894
+ formatColor,
3895
+ clampChannel,
3896
+ shadeColor,
3897
+ rotateVec3,
3898
+ inverseRotateVec3 as inverseRotateVec32,
3899
+ axesHelperPolygons as axesHelperPolygons2,
3900
+ arrowPolygons as arrowPolygons2,
3901
+ ringPolygons as ringPolygons2,
3902
+ octahedronPolygons as octahedronPolygons2,
3903
+ buildSceneContext as buildSceneContext2,
3904
+ computeSceneBbox as computeSceneBbox2,
3905
+ BASE_TILE as BASE_TILE3,
3906
+ DEFAULT_CAMERA_STATE,
3907
+ DEFAULT_PROJECTION,
3908
+ normalizeInvertMultiplier,
3909
+ createPolyAnimationMixer as createPolyAnimationMixer2,
3910
+ LoopOnce,
3911
+ LoopRepeat,
3912
+ LoopPingPong
3913
+ } from "@layoutit/polycss-core";
3914
+ export {
3915
+ BASE_TILE3 as BASE_TILE,
3916
+ DEFAULT_CAMERA_STATE,
3917
+ DEFAULT_PROJECTION,
3918
+ LoopOnce,
3919
+ LoopPingPong,
3920
+ LoopRepeat,
3921
+ Poly,
3922
+ PolyAxesHelper,
3923
+ PolyPerspectiveCamera as PolyCamera,
3924
+ PolyCameraContext,
3925
+ PolyDirectionalLightHelper,
3926
+ PolyMapControls,
3927
+ PolyMesh,
3928
+ PolyOrbitControls,
3929
+ PolyOrthographicCamera,
3930
+ PolyPerspectiveCamera,
3931
+ PolyScene,
3932
+ PolySelect,
3933
+ PolyTransformControls,
3934
+ arrowPolygons2 as arrowPolygons,
3935
+ axesHelperPolygons2 as axesHelperPolygons,
3936
+ bakeSolidTextureSampledPolygons,
3937
+ bakeSolidTextureSamples,
3938
+ buildSceneContext2 as buildSceneContext,
3939
+ clampChannel,
3940
+ computeSceneBbox2 as computeSceneBbox,
3941
+ computeShapeLighting,
3942
+ computeTexturePaintMetrics,
3943
+ coverPlanarPolygons,
3944
+ createIsometricCamera3 as createIsometricCamera,
3945
+ createPolyAnimationMixer2 as createPolyAnimationMixer,
3946
+ cullInteriorPolygons,
3947
+ findMeshUnderPoint,
3948
+ findPolyMeshHandle,
3949
+ formatColor,
3950
+ injectPolyBaseStyles,
3951
+ inverseRotateVec32 as inverseRotateVec3,
3952
+ loadMesh2 as loadMesh,
3953
+ mergePolygons2 as mergePolygons,
3954
+ normalizeInvertMultiplier,
3955
+ normalizePolygons,
3956
+ octahedronPolygons2 as octahedronPolygons,
3957
+ parseColor,
3958
+ parseGltf,
3959
+ parseHexColor2 as parseHexColor,
3960
+ parseMtl,
3961
+ parseObj,
3962
+ parsePureColor3 as parsePureColor,
3963
+ parseRgbColor,
3964
+ parseVox,
3965
+ pointInMeshElement,
3966
+ polygonFaces,
3967
+ ringPolygons2 as ringPolygons,
3968
+ rotateVec3,
3969
+ shadeColor,
3970
+ useCameraContext,
3971
+ usePolyAnimation,
3972
+ usePolyCamera,
3973
+ usePolyMaterial,
3974
+ usePolyMesh,
3975
+ usePolySceneContext,
3976
+ usePolySelect,
3977
+ usePolySelectionApi
3978
+ };