@layoutit/polycss-react 0.0.1 → 0.2.0

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 CHANGED
@@ -20,7 +20,8 @@ import { createIsometricCamera, BASE_TILE } from "@layoutit/polycss-core";
20
20
  import { useSyncExternalStore, useRef, useCallback } from "react";
21
21
  function createSceneStore(initial) {
22
22
  let state = {
23
- cameraState: { ...initial }
23
+ cameraState: { ...initial },
24
+ autoCenterOffset: [0, 0, 0]
24
25
  };
25
26
  const listeners = /* @__PURE__ */ new Set();
26
27
  function notify() {
@@ -39,15 +40,24 @@ function createSceneStore(initial) {
39
40
  return () => listeners.delete(listener);
40
41
  },
41
42
  updateCameraFromRef(handle) {
42
- state = { cameraState: { ...handle.state } };
43
+ state = { ...state, cameraState: { ...handle.state } };
43
44
  notify();
44
45
  return true;
45
46
  },
46
47
  notifyAll() {
47
48
  notify();
49
+ },
50
+ setAutoCenterOffset(offset) {
51
+ state = { ...state, autoCenterOffset: offset };
48
52
  }
49
53
  };
50
54
  }
55
+ function useStoreSelector(store, selector) {
56
+ const selectorRef = useRef(selector);
57
+ selectorRef.current = selector;
58
+ const getSnapshot = useCallback(() => selectorRef.current(store.getState()), [store]);
59
+ return useSyncExternalStore(store.subscribe, getSnapshot);
60
+ }
51
61
 
52
62
  // src/camera/useCamera.ts
53
63
  function usePolyCamera(options) {
@@ -80,11 +90,14 @@ function usePolyCamera(options) {
80
90
  const el = sceneElRef.current;
81
91
  if (el) {
82
92
  const s = handle.state;
93
+ const [ox, oy, oz] = store.getState().autoCenterOffset;
83
94
  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;
95
+ const wx = s.target[0] + ox;
96
+ const wy = s.target[1] + oy;
97
+ const wz = s.target[2] + oz;
98
+ const cssX = wy * tileSize;
99
+ const cssY = wx * tileSize;
100
+ const cssZ = wz * tileSize;
88
101
  const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
89
102
  el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
90
103
  }
@@ -97,14 +110,17 @@ function usePolyCamera(options) {
97
110
  if (!el) return;
98
111
  const handle = handleRef.current;
99
112
  const s = handle.state;
113
+ const [ox, oy, oz] = store.getState().autoCenterOffset;
100
114
  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;
115
+ const wx = s.target[0] + ox;
116
+ const wy = s.target[1] + oy;
117
+ const wz = s.target[2] + oz;
118
+ const cssX = wy * tileSize;
119
+ const cssY = wx * tileSize;
120
+ const cssZ = wz * tileSize;
105
121
  const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
106
122
  el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
107
- }, []);
123
+ }, [store]);
108
124
  return {
109
125
  store,
110
126
  cameraRef: handleRef,
@@ -198,7 +214,7 @@ var PolyOrthographicCamera = memo2(PolyOrthographicCameraInner);
198
214
 
199
215
  // src/scene/PolyScene.tsx
200
216
  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";
217
+ import { BASE_TILE as BASE_TILE2, parseHexColor } from "@layoutit/polycss-core";
202
218
 
203
219
  // src/scene/useSceneContext.ts
204
220
  import { useMemo as useMemo4 } from "react";
@@ -242,12 +258,6 @@ var CORE_BASE_STYLES = `
242
258
  height: 0;
243
259
  transform-style: preserve-3d;
244
260
  perspective: none;
245
- transform: var(--scene-transform);
246
- }
247
-
248
- .polycss-offset {
249
- transform-style: preserve-3d;
250
- transform: var(--offset-transform);
251
261
  }
252
262
 
253
263
  /* \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 */
@@ -270,6 +280,22 @@ var CORE_BASE_STYLES = `
270
280
  position: absolute;
271
281
  }
272
282
 
283
+ /* \u2500\u2500 First-person controls perspective context \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 */
284
+
285
+ /* PolyFirstPersonControls toggles this class on its host element (the camera
286
+ wrapper). FPV needs a real perspective context so scene Z translation
287
+ produces visible depth motion - without it, walking forward looks like a
288
+ planar pan. The class wins over inline perspective styles (e.g.
289
+ PolyOrthographicCamera's perspective: none) via !important. The actual
290
+ perspective value is set inline by the controls as the
291
+ --polycss-fpv-perspective custom property; the default of 2000px matches
292
+ the controls' lookOffset fallback so the FPV math and visual perspective
293
+ stay in sync. */
294
+ .polycss-fpv-host {
295
+ perspective: var(--polycss-fpv-perspective, 2000px) !important;
296
+ transform-style: preserve-3d !important;
297
+ }
298
+
273
299
  /* \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
300
 
275
301
  /*
@@ -297,24 +323,37 @@ var CORE_BASE_STYLES = `
297
323
  background-repeat: no-repeat;
298
324
  }
299
325
 
326
+ .polycss-scene b,
327
+ .polycss-scene i,
328
+ .polycss-scene u {
329
+ color: var(--polycss-paint, currentColor);
330
+ }
331
+
300
332
  .polycss-scene b {
333
+ background: currentColor;
334
+ width: 64px;
335
+ height: 64px;
301
336
  }
302
337
 
303
338
  .polycss-scene i {
304
- border-style: solid;
305
- border-width: 1px;
339
+ width: 16px;
340
+ height: 16px;
306
341
  border-color: currentColor;
307
342
  }
308
343
 
309
344
  .polycss-scene s {
345
+ width: var(--polycss-atlas-size, 64px);
346
+ height: var(--polycss-atlas-size, 64px);
310
347
  }
311
348
 
312
349
  .polycss-scene u {
313
- width: 0px;
314
- height: 0px;
350
+ width: 0;
351
+ height: 0;
315
352
  background: transparent;
316
353
  box-sizing: content-box;
317
354
  border: 0 solid transparent;
355
+ border-color: transparent transparent currentColor transparent;
356
+ border-width: 0 32px 64px 32px;
318
357
  }
319
358
 
320
359
  /* \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 */
@@ -327,17 +366,43 @@ var CORE_BASE_STYLES = `
327
366
  * up half-culled (you see only the side faces, not the caps), and the
328
367
  * arrow looks like a flat strip instead of a 3D bar.
329
368
  *
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.
369
+ * Transitions on color, border-color, and background-color smooth the
370
+ * idle \u2192 hover \u2192 drag alpha changes across rect, border-shape, triangle,
371
+ * and atlas paths.
334
372
  */
335
373
  .polycss-transform-controls i,
336
374
  .polycss-transform-controls b,
337
375
  .polycss-transform-controls s,
338
376
  .polycss-transform-controls u {
339
377
  backface-visibility: visible;
340
- transition: border-color 150ms ease-out, background-color 150ms ease-out;
378
+ transition: color 150ms ease-out, border-color 150ms ease-out, background-color 150ms ease-out;
379
+ }
380
+
381
+ /*
382
+ * Rotate rings are rendered as a single square quad per ring, then masked
383
+ * to a donut via a radial-gradient. --ring-inner-ratio is set inline by
384
+ * <PolyTransformControls> (= innerR / outerR, where outerR is the edge of
385
+ * the quad mapped to 50%). Hit-testing also uses the donut shape \u2014 see
386
+ * the ring-aware path in TransformControls.tsx. Single DOM node per ring.
387
+ */
388
+ .polycss-transform-ring i,
389
+ .polycss-transform-ring b,
390
+ .polycss-transform-ring s,
391
+ .polycss-transform-ring u {
392
+ --ring-inner-r: calc(var(--ring-inner-ratio, 0.92) * 50%);
393
+ --ring-outer-r: calc(var(--ring-outer-ratio, 1) * 50%);
394
+ -webkit-mask: radial-gradient(circle at 50% 50%,
395
+ transparent 0%,
396
+ transparent var(--ring-inner-r),
397
+ black var(--ring-inner-r),
398
+ black var(--ring-outer-r),
399
+ transparent var(--ring-outer-r));
400
+ mask: radial-gradient(circle at 50% 50%,
401
+ transparent 0%,
402
+ transparent var(--ring-inner-r),
403
+ black var(--ring-inner-r),
404
+ black var(--ring-outer-r),
405
+ transparent var(--ring-outer-r));
341
406
  }
342
407
 
343
408
  /* \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 */
@@ -357,6 +422,20 @@ var CORE_BASE_STYLES = `
357
422
  @property --plx { syntax: "<number>"; inherits: true; initial-value: 0; }
358
423
  @property --ply { syntax: "<number>"; inherits: true; initial-value: 0; }
359
424
  @property --plz { syntax: "<number>"; inherits: true; initial-value: 1; }
425
+
426
+ /* CSS-space light components (world-Y\u2192cssX, world-X\u2192cssY, world-Z\u2192cssZ).
427
+ Used by the shadow projection matrix. --clz is clamped away from 0 in JS
428
+ to avoid divide-by-zero when the light is near-horizontal. */
429
+ @property --clx { syntax: "<number>"; inherits: true; initial-value: 0.01; }
430
+ @property --cly { syntax: "<number>"; inherits: true; initial-value: 0; }
431
+ @property --clz { syntax: "<number>"; inherits: true; initial-value: 1; }
432
+
433
+ /* Ground-plane position in CSS pixels along the CSS-Z axis (= world-Z, the
434
+ up axis in polycss's world convention). Stored as a <number> so it can be
435
+ used directly inside matrix3d() calc() expressions (matrix3d requires
436
+ dimensionless entries \u2014 no px units).
437
+ Set by PolyScene from the min world-Z of casting meshes. */
438
+ @property --shadow-ground-cssz { syntax: "<number>"; inherits: true; initial-value: 0; }
360
439
  @property --plr { syntax: "<number>"; inherits: true; initial-value: 1; }
361
440
  @property --plg { syntax: "<number>"; inherits: true; initial-value: 1; }
362
441
  @property --plb { syntax: "<number>"; inherits: true; initial-value: 1; }
@@ -366,14 +445,14 @@ var CORE_BASE_STYLES = `
366
445
  @property --pab { syntax: "<number>"; inherits: true; initial-value: 1; }
367
446
  @property --pai { syntax: "<number>"; inherits: true; initial-value: 0.4; }
368
447
 
369
- /* Per-polygon surface normal \u2014 set inline by the renderer. inherits:false
370
- because each leaf has its own normal (no cascade). */
448
+ /* Per-polygon surface normal \u2014 set inline by the renderer. Base RGB channels
449
+ may be hoisted to a mesh wrapper, so they inherit unless overridden inline. */
371
450
  @property --pnx { syntax: "<number>"; inherits: false; initial-value: 0; }
372
451
  @property --pny { syntax: "<number>"; inherits: false; initial-value: 0; }
373
452
  @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; }
453
+ @property --psr { syntax: "<number>"; inherits: true; initial-value: 1; }
454
+ @property --psg { syntax: "<number>"; inherits: true; initial-value: 1; }
455
+ @property --psb { syntax: "<number>"; inherits: true; initial-value: 1; }
377
456
 
378
457
  /* Calc-driven Lambert + tint, scoped to dynamic-lighting scenes. Lives
379
458
  here (not inline per polygon) so each leaf only carries its tiny normal
@@ -399,8 +478,9 @@ var CORE_BASE_STYLES = `
399
478
  background-blend-mode: multiply;
400
479
  }
401
480
 
481
+ .polycss-scene[data-polycss-lighting="dynamic"] b,
402
482
  .polycss-scene[data-polycss-lighting="dynamic"] u {
403
- border-bottom-color: rgb(
483
+ color: rgb(
404
484
  calc(255 * var(--psr) * (var(--par) * var(--pai)
405
485
  + var(--plr) * var(--pli) * max(0,
406
486
  var(--pnx) * var(--plx) +
@@ -418,6 +498,92 @@ var CORE_BASE_STYLES = `
418
498
  var(--pnz) * var(--plz))))
419
499
  );
420
500
  }
501
+
502
+ /* <q> \u2014 dedicated shadow leaf. Same border-shape rendering trick as <i>
503
+ (border-color: currentColor fills the polygon outline) but with its
504
+ own tag so we don't have to thread :not(.polycss-shadow) exclusions
505
+ through every dynamic-mode color rule. backface-visibility must be
506
+ visible because the projection matrix is near-rank-deficient and the
507
+ resulting plane's normal can read as back-facing under some camera
508
+ angles; the leaf is intentionally always painted. Strip the UA's
509
+ default ::before/::after open-/close-quote so the element is just a
510
+ styled box. */
511
+ .polycss-scene q {
512
+ position: absolute;
513
+ display: block;
514
+ transform-origin: 0 0;
515
+ transform-style: preserve-3d;
516
+ margin: 0;
517
+ padding: 0;
518
+ font: inherit;
519
+ font-weight: normal;
520
+ font-style: normal;
521
+ line-height: 0;
522
+ text-decoration: none;
523
+ backface-visibility: visible;
524
+ border-color: currentColor;
525
+ pointer-events: none;
526
+ will-change: transform;
527
+ }
528
+ .polycss-scene q::before,
529
+ .polycss-scene q::after {
530
+ content: none;
531
+ }
532
+
533
+ /* \u2500\u2500 Cast shadows (dynamic mode only) \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 */
534
+
535
+ /*
536
+ * Shadow projection matrix. Projects any 3D point P onto the horizontal
537
+ * ground plane (cssZ \u2248 G) along the CSS-space light direction (--clx/y/z).
538
+ *
539
+ * In polycss's world convention world Z is up (red-green plane is the
540
+ * floor in the axes helper). After the world\u2192CSS swap (Y\u2194X), world Z stays
541
+ * as CSS Z, so the ground plane normal in CSS space is +cssZ.
542
+ *
543
+ * The strict projection formula would set m22=0 (output.z is a constant G,
544
+ * the polygon is exactly flat). But Chromium SKIPS rendering elements
545
+ * whose composed transform matrix is non-invertible (singular). m22=0
546
+ * makes the matrix singular, so the shadow paints nothing even though it
547
+ * has a valid layout box. The fix: collapse along z by a near-zero
548
+ * scale (Z_SQUASH = 0.01) instead of exactly zero \u2014 output.z is then
549
+ * approximately G with ~1% drift from the input, full-rank and renderable.
550
+ * The shadow still looks flat to the eye (the drift is sub-pixel for
551
+ * any realistic scene size).
552
+ *
553
+ * out.cssX = P.cssX - (--clx/--clz) * (P.cssZ - G)
554
+ * out.cssY = P.cssY - (--cly/--clz) * (P.cssZ - G)
555
+ * out.cssZ = Z_SQUASH * P.cssZ + (1 - Z_SQUASH) * G
556
+ *
557
+ * As column-major 4\xD74 (CSS matrix3d order):
558
+ * col1: [1, 0, 0, 0]
559
+ * col2: [0, 1, 0, 0]
560
+ * col3: [-(--clx/--clz), -(--cly/--clz), Z_SQUASH, 0]
561
+ * col4: [G*(--clx/--clz), G*(--cly/--clz), G*(1-Z_SQUASH), 1]
562
+ */
563
+ .polycss-scene[data-polycss-lighting="dynamic"] {
564
+ --shadow-proj: matrix3d(
565
+ 1, 0, 0, 0,
566
+ 0, 1, 0, 0,
567
+ calc(-1 * var(--clx) / var(--clz)),
568
+ calc(-1 * var(--cly) / var(--clz)),
569
+ 0.01,
570
+ 0,
571
+ calc(var(--shadow-ground-cssz) * var(--clx) / var(--clz)),
572
+ calc(var(--shadow-ground-cssz) * var(--cly) / var(--clz)),
573
+ calc(var(--shadow-ground-cssz) * 0.99),
574
+ 1
575
+ );
576
+ }
577
+
578
+ /* <q> shadow leaf \u2014 Lambert-gated opacity. Polygons facing the light cast
579
+ full shadow; polygons facing away cast zero shadow (their projection
580
+ would stack inside the silhouette and produce ugly overdraw). The
581
+ * 10 multiplier sharpens the cutoff so small positive Lambert values
582
+ jump quickly to 1, giving a near-binary visibility decision with a
583
+ smooth edge transition. Pure CSS calc \u2014 no JS at light-change time. */
584
+ .polycss-scene q {
585
+ opacity: clamp(0, calc((var(--pnx) * var(--clx) + var(--pny) * var(--cly) + var(--pnz) * var(--clz)) * 10), 1);
586
+ }
421
587
  `;
422
588
 
423
589
  // src/scene/textureAtlas.tsx
@@ -437,15 +603,30 @@ var MAX_ATLAS_SCALE = 1;
437
603
  var AUTO_ATLAS_LOW_AREA = ATLAS_MAX_SIZE * ATLAS_MAX_SIZE;
438
604
  var AUTO_ATLAS_MEDIUM_AREA = AUTO_ATLAS_LOW_AREA * 3;
439
605
  var AUTO_ATLAS_MAX_BITMAP_SIDE = 2048;
440
- var AUTO_ATLAS_MAX_DECODED_BYTES = 16 * 1024 * 1024;
606
+ var AUTO_ATLAS_MAX_DECODED_BYTES_MOBILE = 4 * 1024 * 1024;
607
+ var AUTO_ATLAS_MAX_DECODED_BYTES_DESKTOP = 16 * 1024 * 1024;
441
608
  var AUTO_ATLAS_SCALE_GUARD = 0.995;
442
609
  var DEFAULT_MATRIX_DECIMALS = 3;
443
610
  var DEFAULT_BORDER_SHAPE_DECIMALS = 2;
611
+ var DEFAULT_ATLAS_CSS_DECIMALS = 4;
612
+ var SOLID_QUAD_CANONICAL_SIZE = 64;
613
+ var SOLID_TRIANGLE_CANONICAL_SIZE = 64;
614
+ var ATLAS_CANONICAL_SIZE_EXPLICIT = 64;
615
+ var ATLAS_CANONICAL_SIZE_AUTO_DESKTOP = 128;
616
+ var BORDER_SHAPE_CENTER_PERCENT = 50;
617
+ var BORDER_SHAPE_POINT_EPS = 1e-7;
618
+ var BORDER_SHAPE_CANONICAL_SIZE = 16;
619
+ var PROJECTIVE_QUAD_DENOM_EPS = 0.05;
620
+ var PROJECTIVE_QUAD_MAX_WEIGHT_RATIO = 4;
621
+ var PROJECTIVE_QUAD_BLEED = 0.6;
444
622
  var BASIS_EPS = 1e-9;
445
- var SOLID_TRIANGLE_BLEED = 0.45;
623
+ var SOLID_TRIANGLE_BLEED = 0.6;
446
624
  var TEXTURE_IMAGE_CACHE = /* @__PURE__ */ new Map();
447
625
  var RECT_EPS = 1e-3;
448
626
  var TEXTURE_TRIANGLE_BLEED = 0.75;
627
+ var TEXTURE_EDGE_REPAIR_ALPHA_MIN = 1;
628
+ var TEXTURE_EDGE_REPAIR_SOURCE_ALPHA_MIN = 250;
629
+ var TEXTURE_EDGE_REPAIR_RADIUS = 1.5;
449
630
  function loadTextureImage(url) {
450
631
  let p = TEXTURE_IMAGE_CACHE.get(url);
451
632
  if (!p) {
@@ -477,6 +658,10 @@ function roundDecimal(value, decimals) {
477
658
  const next = value.toFixed(decimals).replace(/\.?0+$/, "");
478
659
  return Object.is(Number(next), -0) ? "0" : next;
479
660
  }
661
+ function formatCssLength(value, decimals = DEFAULT_ATLAS_CSS_DECIMALS) {
662
+ const next = roundDecimal(value, decimals);
663
+ return Number(next) === 0 || Object.is(Number(next), -0) ? "0" : `${next}px`;
664
+ }
480
665
  function formatMatrix3d(matrix, decimals = DEFAULT_MATRIX_DECIMALS) {
481
666
  return `matrix3d(${matrix.split(",").map((value) => {
482
667
  const parsed = Number(value.trim());
@@ -484,12 +669,31 @@ function formatMatrix3d(matrix, decimals = DEFAULT_MATRIX_DECIMALS) {
484
669
  }).join(",")})`;
485
670
  }
486
671
  function formatPercent(value, decimals = DEFAULT_BORDER_SHAPE_DECIMALS) {
487
- return `${roundDecimal(value, decimals)}%`;
672
+ const next = roundDecimal(value, decimals);
673
+ return Number(next) === 0 ? "0" : `${next}%`;
674
+ }
675
+ function pointOnSegment(px, py, ax, ay, bx, by) {
676
+ const cross = (px - ax) * (by - ay) - (py - ay) * (bx - ax);
677
+ if (Math.abs(cross) > BORDER_SHAPE_POINT_EPS) return false;
678
+ const dot = (px - ax) * (px - bx) + (py - ay) * (py - by);
679
+ return dot <= BORDER_SHAPE_POINT_EPS;
680
+ }
681
+ function polygonContainsPoint(points, px = BORDER_SHAPE_CENTER_PERCENT, py = BORDER_SHAPE_CENTER_PERCENT) {
682
+ let inside = false;
683
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
684
+ const [xi, yi] = points[i];
685
+ const [xj, yj] = points[j];
686
+ if (pointOnSegment(px, py, xi, yi, xj, yj)) return true;
687
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
688
+ inside = !inside;
689
+ }
690
+ }
691
+ return inside;
488
692
  }
489
693
  function atlasArea(pages) {
490
694
  return pages.reduce((sum, page) => sum + page.width * page.height, 0);
491
695
  }
492
- function autoAtlasScaleCap(pages) {
696
+ function autoAtlasScaleCap(pages, maxDecodedBytes) {
493
697
  const area = atlasArea(pages);
494
698
  if (area <= 0) return 1;
495
699
  const maxSide = Math.max(
@@ -497,15 +701,52 @@ function autoAtlasScaleCap(pages) {
497
701
  ...pages.map((page) => Math.max(page.width, page.height))
498
702
  );
499
703
  const sideScale = AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide;
500
- const memoryScale = Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / (area * 4));
704
+ const memoryScale = Math.sqrt(maxDecodedBytes / (area * 4));
501
705
  return normalizeAtlasScale(Math.min(sideScale, memoryScale));
502
706
  }
503
- function autoAtlasScale(pages) {
707
+ function isMobileDocument(doc) {
708
+ if (!doc) return false;
709
+ const win = doc.defaultView ?? (typeof window !== "undefined" ? window : void 0);
710
+ const media = win?.matchMedia;
711
+ if (!media) return false;
712
+ return media("(pointer: coarse)").matches || media("(hover: none)").matches;
713
+ }
714
+ function autoAtlasMaxDecodedBytes(doc) {
715
+ return isMobileDocument(doc) ? AUTO_ATLAS_MAX_DECODED_BYTES_MOBILE : AUTO_ATLAS_MAX_DECODED_BYTES_DESKTOP;
716
+ }
717
+ function atlasCanonicalSizeForTextureQuality(textureQualityInput, doc) {
718
+ if (textureQualityInput !== void 0 && textureQualityInput !== "auto") {
719
+ return ATLAS_CANONICAL_SIZE_EXPLICIT;
720
+ }
721
+ return isMobileDocument(doc) ? ATLAS_CANONICAL_SIZE_EXPLICIT : ATLAS_CANONICAL_SIZE_AUTO_DESKTOP;
722
+ }
723
+ function formatAtlasMatrix(entry, atlasCanonicalSize) {
724
+ const values = entry.matrix.split(",").map((value) => Number(value));
725
+ if (values.length !== 16 || values.some((value) => !Number.isFinite(value))) {
726
+ return entry.canonicalMatrix;
727
+ }
728
+ values[0] *= entry.canvasW / atlasCanonicalSize;
729
+ values[1] *= entry.canvasW / atlasCanonicalSize;
730
+ values[2] *= entry.canvasW / atlasCanonicalSize;
731
+ values[4] *= entry.canvasH / atlasCanonicalSize;
732
+ values[5] *= entry.canvasH / atlasCanonicalSize;
733
+ values[6] *= entry.canvasH / atlasCanonicalSize;
734
+ return values.join(",");
735
+ }
736
+ function applyPackedAtlasCanonicalSize(packed, atlasCanonicalSize) {
737
+ for (const entry of packed.entries) {
738
+ if (!entry) continue;
739
+ entry.atlasCanonicalSize = atlasCanonicalSize;
740
+ entry.atlasMatrix = formatAtlasMatrix(entry, atlasCanonicalSize);
741
+ }
742
+ return packed;
743
+ }
744
+ function autoAtlasScale(pages, maxDecodedBytes) {
504
745
  const area = atlasArea(pages);
505
746
  let atlasScale = 0.5;
506
747
  if (area <= AUTO_ATLAS_LOW_AREA) atlasScale = 1;
507
748
  else if (area <= AUTO_ATLAS_MEDIUM_AREA) atlasScale = 0.75;
508
- return normalizeAtlasScale(Math.min(atlasScale, autoAtlasScaleCap(pages)));
749
+ return normalizeAtlasScale(Math.min(atlasScale, autoAtlasScaleCap(pages, maxDecodedBytes)));
509
750
  }
510
751
  function atlasBitmapMaxSide(pages, atlasScale) {
511
752
  return pages.reduce((max, page) => Math.max(
@@ -520,18 +761,18 @@ function atlasDecodedBytes(pages, atlasScale) {
520
761
  0
521
762
  );
522
763
  }
523
- function autoAtlasBudgetFactor(pages, atlasScale) {
764
+ function autoAtlasBudgetFactor(pages, atlasScale, maxDecodedBytes) {
524
765
  const maxSide = atlasBitmapMaxSide(pages, atlasScale);
525
766
  const decodedBytes = atlasDecodedBytes(pages, atlasScale);
526
767
  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;
768
+ const memoryFactor = decodedBytes > maxDecodedBytes ? Math.sqrt(maxDecodedBytes / decodedBytes) : 1;
528
769
  return Math.min(sideFactor, memoryFactor);
529
770
  }
530
- function packTextureAtlasPlansAuto(plans, fullScalePacked) {
531
- let atlasScale = autoAtlasScale(fullScalePacked.pages);
771
+ function packTextureAtlasPlansAuto(plans, fullScalePacked, maxDecodedBytes) {
772
+ let atlasScale = autoAtlasScale(fullScalePacked.pages, maxDecodedBytes);
532
773
  let packed = atlasScale === 1 ? fullScalePacked : packTextureAtlasPlans(plans, atlasScale);
533
774
  for (let i = 0; i < 4; i++) {
534
- const factor = autoAtlasBudgetFactor(packed.pages, atlasScale);
775
+ const factor = autoAtlasBudgetFactor(packed.pages, atlasScale, maxDecodedBytes);
535
776
  if (factor >= 1) break;
536
777
  const nextAtlasScale = normalizeAtlasScale(atlasScale * factor * AUTO_ATLAS_SCALE_GUARD);
537
778
  if (nextAtlasScale >= atlasScale) break;
@@ -540,13 +781,23 @@ function packTextureAtlasPlansAuto(plans, fullScalePacked) {
540
781
  }
541
782
  return { packed, atlasScale };
542
783
  }
543
- function packTextureAtlasPlansWithScale(plans, atlasScaleInput) {
544
- if (atlasScaleInput !== void 0 && atlasScaleInput !== "auto") {
545
- const atlasScale = normalizeAtlasScale(atlasScaleInput);
546
- return { packed: packTextureAtlasPlans(plans, atlasScale), atlasScale };
784
+ function packTextureAtlasPlansWithScale(plans, textureQualityInput, doc) {
785
+ const atlasCanonicalSize = atlasCanonicalSizeForTextureQuality(textureQualityInput, doc);
786
+ if (textureQualityInput !== void 0 && textureQualityInput !== "auto") {
787
+ const atlasScale = normalizeAtlasScale(textureQualityInput);
788
+ return {
789
+ packed: applyPackedAtlasCanonicalSize(packTextureAtlasPlans(plans, atlasScale), atlasCanonicalSize),
790
+ atlasScale,
791
+ atlasCanonicalSize
792
+ };
547
793
  }
548
794
  const fullScalePacked = packTextureAtlasPlans(plans, 1);
549
- return packTextureAtlasPlansAuto(plans, fullScalePacked);
795
+ const autoPacked = packTextureAtlasPlansAuto(plans, fullScalePacked, autoAtlasMaxDecodedBytes(doc));
796
+ return {
797
+ packed: applyPackedAtlasCanonicalSize(autoPacked.packed, atlasCanonicalSize),
798
+ atlasScale: autoPacked.atlasScale,
799
+ atlasCanonicalSize
800
+ };
550
801
  }
551
802
  function atlasPadding(atlasScale) {
552
803
  return Math.max(ATLAS_PADDING, Math.ceil(ATLAS_PADDING / atlasScale));
@@ -566,6 +817,9 @@ function parseHex(hex) {
566
817
  if (!parsed) return { r: 255, g: 255, b: 255 };
567
818
  return { r: parsed.rgb[0], g: parsed.rgb[1], b: parsed.rgb[2] };
568
819
  }
820
+ function rgbKey({ r, g, b }) {
821
+ return `${r},${g},${b}`;
822
+ }
569
823
  function parseAlpha(input) {
570
824
  return parsePureColor(input)?.alpha ?? 1;
571
825
  }
@@ -601,60 +855,369 @@ function isFullRectSolid(entry) {
601
855
  function isSolidTrianglePlan(entry) {
602
856
  return !entry.texture && entry.polygon.vertices.length === 3;
603
857
  }
858
+ function isProjectiveQuadPlan(entry) {
859
+ return !entry.texture && !!entry.projectiveMatrix && !isFullRectSolid(entry);
860
+ }
604
861
  function borderShapeSupported() {
605
862
  const supportsBorderShape = !!globalThis.CSS?.supports?.(
606
863
  "border-shape",
607
- "polygon(0 0, 100% 0, 0 100%) polygon(50% 50%, 50% 50%, 50% 50%)"
864
+ "polygon(0 0, 100% 0, 0 100%) circle(0)"
608
865
  );
609
866
  if (!supportsBorderShape) return false;
610
867
  const media = globalThis.matchMedia;
611
868
  if (typeof media !== "function") return true;
612
869
  return media("(pointer: fine)").matches && media("(hover: hover)").matches;
613
870
  }
614
- function cssPolygonShapeForPlan(entry) {
615
- const pts = [];
871
+ function solidTriangleSupported() {
872
+ const userAgent = (typeof window !== "undefined" ? window.navigator : globalThis.navigator)?.userAgent ?? "";
873
+ if (!userAgent) return true;
874
+ const isChromiumFamily = /\b(?:Chrome|HeadlessChrome|Chromium|Edg|OPR)\//.test(userAgent);
875
+ const isSafariFamily = /\bVersion\/[\d.]+.*\bSafari\//.test(userAgent);
876
+ return !isSafariFamily || isChromiumFamily;
877
+ }
878
+ function incrementCount(map, key) {
879
+ map.set(key, (map.get(key) ?? 0) + 1);
880
+ }
881
+ function dominantCountKey(map) {
882
+ let bestKey;
883
+ let bestCount = 1;
884
+ for (const [key, count] of map) {
885
+ if (count > bestCount) {
886
+ bestKey = key;
887
+ bestCount = count;
888
+ }
889
+ }
890
+ return bestKey;
891
+ }
892
+ var BRUSH_INLINE_STYLE_ORDER = /* @__PURE__ */ new Map([
893
+ ["transform", 0],
894
+ ["border-shape", 1],
895
+ ["border-width", 2],
896
+ ["width", 3],
897
+ ["height", 4],
898
+ ["color", 5]
899
+ ]);
900
+ function orderBrushInlineStyle(el) {
901
+ const current = el.getAttribute("style");
902
+ if (!current) return;
903
+ const declarations = current.split(";").map((declaration) => declaration.trim()).filter(Boolean);
904
+ const next = declarations.map((declaration, index) => {
905
+ const property = declaration.slice(0, declaration.indexOf(":")).trim().toLowerCase();
906
+ return {
907
+ declaration,
908
+ index,
909
+ order: BRUSH_INLINE_STYLE_ORDER.get(property) ?? Number.POSITIVE_INFINITY
910
+ };
911
+ }).sort((a, b) => a.order - b.order || a.index - b.index).map(({ declaration }) => declaration).join(";");
912
+ if (next !== current) el.setAttribute("style", next);
913
+ }
914
+ function getSolidPaintDefaults(plans, textureLighting) {
915
+ const paintCounts = /* @__PURE__ */ new Map();
916
+ const dynamicCounts = /* @__PURE__ */ new Map();
917
+ const dynamicColors = /* @__PURE__ */ new Map();
918
+ const useStableTriangle = solidTriangleSupported();
919
+ const useBorderShape = textureLighting !== "dynamic" && borderShapeSupported();
920
+ for (const plan of plans) {
921
+ if (!plan || plan.texture) continue;
922
+ if (textureLighting === "dynamic") {
923
+ if (!(useStableTriangle && isSolidTrianglePlan(plan)) && !isFullRectSolid(plan)) continue;
924
+ const color = parseHex(plan.polygon.color ?? "#cccccc");
925
+ const key = rgbKey(color);
926
+ incrementCount(dynamicCounts, key);
927
+ if (!dynamicColors.has(key)) dynamicColors.set(key, color);
928
+ continue;
929
+ }
930
+ if (!(useStableTriangle && isSolidTrianglePlan(plan)) && !isFullRectSolid(plan) && !useBorderShape) continue;
931
+ incrementCount(paintCounts, plan.shadedColor);
932
+ }
933
+ const paintColor = dominantCountKey(paintCounts);
934
+ const dynamicColorKey = dominantCountKey(dynamicCounts);
935
+ return {
936
+ paintColor,
937
+ dynamicColorKey,
938
+ dynamicColor: dynamicColorKey ? dynamicColors.get(dynamicColorKey) : void 0
939
+ };
940
+ }
941
+ function borderShapePointsForPlan(entry) {
942
+ const points = [];
616
943
  const width = entry.canvasW || 1;
617
944
  const height = entry.canvasH || 1;
618
945
  for (let i = 0; i < entry.screenPts.length; i += 2) {
619
946
  const x = Math.max(0, Math.min(100, entry.screenPts[i] / width * 100));
620
947
  const y = Math.max(0, Math.min(100, entry.screenPts[i + 1] / height * 100));
621
- pts.push(`${formatPercent(x)} ${formatPercent(y)}`);
948
+ points.push([x, y]);
622
949
  }
623
- return `polygon(${pts.join(", ")})`;
950
+ return points;
951
+ }
952
+ function cssBorderShapePoint([x, y]) {
953
+ return `${formatPercent(x)} ${formatPercent(y)}`;
624
954
  }
625
- function cssCollapsedInnerShapeForPlan(entry) {
955
+ function cssPolygonShapeForPoints(points) {
956
+ return `polygon(${points.map(cssBorderShapePoint).join(",")})`;
957
+ }
958
+ function cssCollapsedInnerShapeForPoints(points) {
959
+ if (polygonContainsPoint(points)) return "circle(0)";
626
960
  let xSum = 0;
627
961
  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];
962
+ const pointCount = Math.max(1, points.length);
963
+ for (const [x2, y2] of points) {
964
+ xSum += x2;
965
+ ySum += y2;
632
966
  }
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(", ")})`;
967
+ const x = formatPercent(Math.max(0, Math.min(100, xSum / pointCount)));
968
+ const y = formatPercent(Math.max(0, Math.min(100, ySum / pointCount)));
969
+ return `circle(0 at ${x} ${y})`;
638
970
  }
639
971
  function cssBorderShapeForPlan(entry) {
640
- return `${cssPolygonShapeForPlan(entry)} ${cssCollapsedInnerShapeForPlan(entry)}`;
972
+ const points = borderShapePointsForPlan(entry);
973
+ return `${cssPolygonShapeForPoints(points)} ${cssCollapsedInnerShapeForPoints(points)}`;
641
974
  }
642
975
  function formatMatrix3dValues(values, decimals = DEFAULT_MATRIX_DECIMALS) {
643
976
  return values.map((value) => roundDecimal(value, decimals)).join(",");
644
977
  }
978
+ function formatScaledMatrixFromPlan(entry, scaleX, scaleY) {
979
+ const values = entry.matrix.split(",").map((value) => Number(value));
980
+ if (values.length !== 16 || values.some((value) => !Number.isFinite(value))) {
981
+ return entry.matrix;
982
+ }
983
+ values[0] *= scaleX;
984
+ values[1] *= scaleX;
985
+ values[2] *= scaleX;
986
+ values[4] *= scaleY;
987
+ values[5] *= scaleY;
988
+ values[6] *= scaleY;
989
+ return formatMatrix3dValues(values);
990
+ }
991
+ function formatBorderShapeMatrix(entry) {
992
+ return formatScaledMatrixFromPlan(
993
+ entry,
994
+ (entry.canvasW || 1) / BORDER_SHAPE_CANONICAL_SIZE,
995
+ (entry.canvasH || 1) / BORDER_SHAPE_CANONICAL_SIZE
996
+ );
997
+ }
998
+ function formatSolidQuadMatrix(entry) {
999
+ return formatScaledMatrixFromPlan(
1000
+ entry,
1001
+ (entry.canvasW || 1) / SOLID_QUAD_CANONICAL_SIZE,
1002
+ (entry.canvasH || 1) / SOLID_QUAD_CANONICAL_SIZE
1003
+ );
1004
+ }
1005
+ function isConvexPolygonPoints(points) {
1006
+ if (points.length < 3) return false;
1007
+ let sign = 0;
1008
+ for (let i = 0; i < points.length; i++) {
1009
+ const a = points[i];
1010
+ const b = points[(i + 1) % points.length];
1011
+ const c = points[(i + 2) % points.length];
1012
+ const cross = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
1013
+ if (Math.abs(cross) <= BASIS_EPS) return false;
1014
+ const nextSign = Math.sign(cross);
1015
+ if (sign === 0) sign = nextSign;
1016
+ else if (nextSign !== sign) return false;
1017
+ }
1018
+ return true;
1019
+ }
1020
+ function signedArea2D(points) {
1021
+ let area = 0;
1022
+ for (let i = 0; i < points.length; i++) {
1023
+ const a = points[i];
1024
+ const b = points[(i + 1) % points.length];
1025
+ area += a[0] * b[1] - a[1] * b[0];
1026
+ }
1027
+ return area / 2;
1028
+ }
1029
+ function intersect2DLines(a0, a1, b0, b1) {
1030
+ const rx = a1[0] - a0[0];
1031
+ const ry = a1[1] - a0[1];
1032
+ const sx = b1[0] - b0[0];
1033
+ const sy = b1[1] - b0[1];
1034
+ const det = rx * sy - ry * sx;
1035
+ if (Math.abs(det) <= BASIS_EPS) return null;
1036
+ const qpx = b0[0] - a0[0];
1037
+ const qpy = b0[1] - a0[1];
1038
+ const t = (qpx * sy - qpy * sx) / det;
1039
+ return [a0[0] + t * rx, a0[1] + t * ry];
1040
+ }
1041
+ function offsetConvexPolygonPoints(points, amount) {
1042
+ if (points.length < 6 || points.length % 2 !== 0 || amount <= 0) return points;
1043
+ const q = [];
1044
+ for (let i = 0; i < points.length; i += 2) q.push([points[i], points[i + 1]]);
1045
+ if (!isConvexPolygonPoints(q)) return expandClipPoints(points, amount);
1046
+ const area = signedArea2D(q);
1047
+ if (Math.abs(area) <= BASIS_EPS) return expandClipPoints(points, amount);
1048
+ const outwardSign = area > 0 ? 1 : -1;
1049
+ const offsetLines = [];
1050
+ for (let i = 0; i < q.length; i++) {
1051
+ const a = q[i];
1052
+ const b = q[(i + 1) % q.length];
1053
+ const dx = b[0] - a[0];
1054
+ const dy = b[1] - a[1];
1055
+ const length = Math.hypot(dx, dy);
1056
+ if (length <= BASIS_EPS) return expandClipPoints(points, amount);
1057
+ const ox = outwardSign * (dy / length) * amount;
1058
+ const oy = outwardSign * (-dx / length) * amount;
1059
+ offsetLines.push({
1060
+ a: [a[0] + ox, a[1] + oy],
1061
+ b: [b[0] + ox, b[1] + oy]
1062
+ });
1063
+ }
1064
+ const expanded = [];
1065
+ const maxMiter = Math.max(2, amount * 4);
1066
+ for (let i = 0; i < q.length; i++) {
1067
+ const prev = offsetLines[(i + q.length - 1) % q.length];
1068
+ const next = offsetLines[i];
1069
+ const intersection = intersect2DLines(prev.a, prev.b, next.a, next.b);
1070
+ if (!intersection) return expandClipPoints(points, amount);
1071
+ const original = q[i];
1072
+ const dx = intersection[0] - original[0];
1073
+ const dy = intersection[1] - original[1];
1074
+ const miter = Math.hypot(dx, dy);
1075
+ if (miter > maxMiter) {
1076
+ expanded.push(
1077
+ original[0] + dx / miter * maxMiter,
1078
+ original[1] + dy / miter * maxMiter
1079
+ );
1080
+ } else {
1081
+ expanded.push(intersection[0], intersection[1]);
1082
+ }
1083
+ }
1084
+ return expanded;
1085
+ }
1086
+ function computeProjectiveQuadCoefficients(q) {
1087
+ if (q.length !== 4 || !isConvexPolygonPoints(q)) return null;
1088
+ const [q0, q1, q2, q3] = q;
1089
+ const sx = q0[0] - q1[0] + q2[0] - q3[0];
1090
+ const sy = q0[1] - q1[1] + q2[1] - q3[1];
1091
+ const dx1 = q1[0] - q2[0];
1092
+ const dx2 = q3[0] - q2[0];
1093
+ const dy1 = q1[1] - q2[1];
1094
+ const dy2 = q3[1] - q2[1];
1095
+ const det = dx1 * dy2 - dy1 * dx2;
1096
+ if (Math.abs(det) <= BASIS_EPS) return null;
1097
+ const g = (sx * dy2 - sy * dx2) / det;
1098
+ const h = (dx1 * sy - dy1 * sx) / det;
1099
+ const weights = [1, 1 + g, 1 + g + h, 1 + h];
1100
+ if (weights.some((weight) => !Number.isFinite(weight) || weight <= PROJECTIVE_QUAD_DENOM_EPS)) {
1101
+ return null;
1102
+ }
1103
+ const minWeight = Math.min(...weights);
1104
+ const maxWeight = Math.max(...weights);
1105
+ if (maxWeight / minWeight > PROJECTIVE_QUAD_MAX_WEIGHT_RATIO) return null;
1106
+ return {
1107
+ g,
1108
+ h,
1109
+ w1: 1 + g,
1110
+ w3: 1 + h
1111
+ };
1112
+ }
1113
+ function computeProjectiveQuadMatrix(screenPts, xAxis, yAxis, normal, tx, ty, tz) {
1114
+ if (screenPts.length !== 8) return null;
1115
+ const rawQ = [
1116
+ [screenPts[0], screenPts[1]],
1117
+ [screenPts[2], screenPts[3]],
1118
+ [screenPts[4], screenPts[5]],
1119
+ [screenPts[6], screenPts[7]]
1120
+ ];
1121
+ if (!computeProjectiveQuadCoefficients(rawQ)) return null;
1122
+ const expandedPts = offsetConvexPolygonPoints(screenPts, PROJECTIVE_QUAD_BLEED);
1123
+ const q = [
1124
+ [expandedPts[0], expandedPts[1]],
1125
+ [expandedPts[2], expandedPts[3]],
1126
+ [expandedPts[4], expandedPts[5]],
1127
+ [expandedPts[6], expandedPts[7]]
1128
+ ];
1129
+ const coeffs = computeProjectiveQuadCoefficients(q);
1130
+ if (!coeffs) return null;
1131
+ const { g, h, w1, w3 } = coeffs;
1132
+ const [q0, q1, , q3] = q;
1133
+ const toCssPoint = ([x, y]) => [
1134
+ tx + x * xAxis[0] + y * yAxis[0],
1135
+ ty + x * xAxis[1] + y * yAxis[1],
1136
+ tz + x * xAxis[2] + y * yAxis[2]
1137
+ ];
1138
+ const p0 = toCssPoint(q0);
1139
+ const p1 = toCssPoint(q1);
1140
+ const p3 = toCssPoint(q3);
1141
+ const xCol = [
1142
+ p1[0] * w1 - p0[0],
1143
+ p1[1] * w1 - p0[1],
1144
+ p1[2] * w1 - p0[2]
1145
+ ];
1146
+ const yCol = [
1147
+ p3[0] * w3 - p0[0],
1148
+ p3[1] * w3 - p0[1],
1149
+ p3[2] * w3 - p0[2]
1150
+ ];
1151
+ const values = [
1152
+ xCol[0],
1153
+ xCol[1],
1154
+ xCol[2],
1155
+ g,
1156
+ yCol[0],
1157
+ yCol[1],
1158
+ yCol[2],
1159
+ h,
1160
+ normal[0],
1161
+ normal[1],
1162
+ normal[2],
1163
+ 0,
1164
+ p0[0],
1165
+ p0[1],
1166
+ p0[2],
1167
+ 1
1168
+ ];
1169
+ for (let i = 0; i < 8; i += 1) values[i] /= SOLID_QUAD_CANONICAL_SIZE;
1170
+ return values.join(",");
1171
+ }
645
1172
  function cssPoints(vertices, tile, elev) {
646
1173
  return vertices.map((v) => [v[1] * tile, v[0] * tile, v[2] * elev]);
647
1174
  }
1175
+ function pointKey(point) {
1176
+ return `${point[0]},${point[1]},${point[2]}`;
1177
+ }
1178
+ function edgeKey(a, b) {
1179
+ const ak = pointKey(a);
1180
+ const bk = pointKey(b);
1181
+ return ak < bk ? `${ak}|${bk}` : `${bk}|${ak}`;
1182
+ }
1183
+ function buildTextureEdgeRepairSets(polygons) {
1184
+ const edgeOwners = /* @__PURE__ */ new Map();
1185
+ for (let polygonIndex = 0; polygonIndex < polygons.length; polygonIndex++) {
1186
+ const vertices = polygons[polygonIndex].vertices;
1187
+ if (!vertices || vertices.length < 3 || !polygons[polygonIndex].texture) continue;
1188
+ for (let edgeIndex = 0; edgeIndex < vertices.length; edgeIndex++) {
1189
+ const key = edgeKey(vertices[edgeIndex], vertices[(edgeIndex + 1) % vertices.length]);
1190
+ const owner = { polygon: polygonIndex, edge: edgeIndex };
1191
+ const owners = edgeOwners.get(key);
1192
+ if (owners) owners.push(owner);
1193
+ else edgeOwners.set(key, [owner]);
1194
+ }
1195
+ }
1196
+ const repairEdges = polygons.map(() => /* @__PURE__ */ new Set());
1197
+ for (const owners of edgeOwners.values()) {
1198
+ if (owners.length < 2) continue;
1199
+ for (let i = 0; i < owners.length; i++) {
1200
+ for (let j = i + 1; j < owners.length; j++) {
1201
+ repairEdges[owners[i].polygon].add(owners[i].edge);
1202
+ repairEdges[owners[j].polygon].add(owners[j].edge);
1203
+ }
1204
+ }
1205
+ }
1206
+ return repairEdges.map((edges) => edges.size > 0 ? edges : void 0);
1207
+ }
648
1208
  function computeSurfaceNormal(pts) {
649
1209
  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
- ];
1210
+ const p0 = pts[0];
1211
+ const normal = [0, 0, 0];
1212
+ for (let i = 1; i + 1 < pts.length; i++) {
1213
+ const p1 = pts[i];
1214
+ const p2 = pts[i + 1];
1215
+ const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
1216
+ const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
1217
+ normal[0] -= e1[1] * e2[2] - e1[2] * e2[1];
1218
+ normal[1] -= e1[2] * e2[0] - e1[0] * e2[2];
1219
+ normal[2] -= e1[0] * e2[1] - e1[1] * e2[0];
1220
+ }
658
1221
  const len = Math.hypot(normal[0], normal[1], normal[2]);
659
1222
  if (len <= BASIS_EPS) return null;
660
1223
  return [normal[0] / len, normal[1] / len, normal[2] / len];
@@ -669,7 +1232,7 @@ function crossVec(a, b) {
669
1232
  a[0] * b[1] - a[1] * b[0]
670
1233
  ];
671
1234
  }
672
- function solidTriangleStyle(entry, textureLighting, pointerEvents) {
1235
+ function solidTriangleStyle(entry, textureLighting, pointerEvents, solidPaintDefaults) {
673
1236
  if (!isSolidTrianglePlan(entry)) return null;
674
1237
  const pts = cssPoints(entry.polygon.vertices, entry.tileSize, entry.layerElevation);
675
1238
  const normal = computeSurfaceNormal(pts);
@@ -753,47 +1316,89 @@ function solidTriangleStyle(entry, textureLighting, pointerEvents) {
753
1316
  }
754
1317
  const left = Math.max(0, Math.min(baseLength, apexX));
755
1318
  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],
1319
+ const expanded = offsetConvexPolygonPoints([
1320
+ left,
767
1321
  0,
768
- yAxis[0],
769
- yAxis[1],
770
- yAxis[2],
771
1322
  0,
772
- normal[0],
773
- normal[1],
774
- normal[2],
775
- 0,
776
- tx,
777
- ty,
778
- tz,
779
- 1
780
- ]);
1323
+ height,
1324
+ left + right,
1325
+ height
1326
+ ], SOLID_TRIANGLE_BLEED);
1327
+ const apex2 = [expanded[0], expanded[1]];
1328
+ const baseLeft2 = [expanded[2], expanded[3]];
1329
+ const baseRight2 = [expanded[4], expanded[5]];
1330
+ const baseY = (baseLeft2[1] + baseRight2[1]) / 2;
1331
+ const leftPx = apex2[0] - baseLeft2[0];
1332
+ const rightPx = baseRight2[0] - apex2[0];
1333
+ const heightPx = baseY - apex2[1];
1334
+ if (leftPx <= BASIS_EPS || rightPx <= BASIS_EPS || heightPx <= BASIS_EPS || !Number.isFinite(leftPx + rightPx + heightPx)) {
1335
+ return null;
1336
+ }
781
1337
  const dynamic = textureLighting === "dynamic";
782
1338
  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,
1339
+ const useDefaultDynamicColor = dynamic && rgbKey(base) === solidPaintDefaults?.dynamicColorKey;
1340
+ const sharedStyle = {
1341
+ color: dynamic || entry.shadedColor === solidPaintDefaults?.paintColor ? void 0 : entry.shadedColor,
787
1342
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
788
- ...dynamic ? {
1343
+ ...dynamic && !useDefaultDynamicColor ? {
789
1344
  ["--pnx"]: normal[0].toFixed(4),
790
1345
  ["--pny"]: normal[1].toFixed(4),
791
1346
  ["--pnz"]: normal[2].toFixed(4),
792
1347
  ["--psr"]: (base.r / 255).toFixed(4),
793
1348
  ["--psg"]: (base.g / 255).toFixed(4),
794
1349
  ["--psb"]: (base.b / 255).toFixed(4)
1350
+ } : dynamic ? {
1351
+ ["--pnx"]: normal[0].toFixed(4),
1352
+ ["--pny"]: normal[1].toFixed(4),
1353
+ ["--pnz"]: normal[2].toFixed(4)
795
1354
  } : null
796
1355
  };
1356
+ const worldPoint = ([x, y]) => [
1357
+ cv[0] + (x - left) * xAxis[0] + y * yAxis[0],
1358
+ cv[1] + (x - left) * xAxis[1] + y * yAxis[1],
1359
+ cv[2] + (x - left) * xAxis[2] + y * yAxis[2]
1360
+ ];
1361
+ const apex = worldPoint(apex2);
1362
+ const baseLeft = worldPoint([baseLeft2[0], baseY]);
1363
+ const baseRight = worldPoint([baseRight2[0], baseY]);
1364
+ const halfBase = SOLID_TRIANGLE_CANONICAL_SIZE / 2;
1365
+ const xCol = [
1366
+ (baseRight[0] - baseLeft[0]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1367
+ (baseRight[1] - baseLeft[1]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1368
+ (baseRight[2] - baseLeft[2]) / SOLID_TRIANGLE_CANONICAL_SIZE
1369
+ ];
1370
+ const txCol = [
1371
+ apex[0] - xCol[0] * halfBase,
1372
+ apex[1] - xCol[1] * halfBase,
1373
+ apex[2] - xCol[2] * halfBase
1374
+ ];
1375
+ const yCol = [
1376
+ (baseLeft[0] - txCol[0]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1377
+ (baseLeft[1] - txCol[1]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1378
+ (baseLeft[2] - txCol[2]) / SOLID_TRIANGLE_CANONICAL_SIZE
1379
+ ];
1380
+ const canonicalMatrix = formatMatrix3dValues([
1381
+ xCol[0],
1382
+ xCol[1],
1383
+ xCol[2],
1384
+ 0,
1385
+ yCol[0],
1386
+ yCol[1],
1387
+ yCol[2],
1388
+ 0,
1389
+ normal[0],
1390
+ normal[1],
1391
+ normal[2],
1392
+ 0,
1393
+ txCol[0],
1394
+ txCol[1],
1395
+ txCol[2],
1396
+ 1
1397
+ ]);
1398
+ return {
1399
+ transform: `matrix3d(${canonicalMatrix})`,
1400
+ ...sharedStyle
1401
+ };
797
1402
  }
798
1403
  function rgbToHex({ r, g, b }) {
799
1404
  const f = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
@@ -936,6 +1541,15 @@ function expandClipPoints(points, amount) {
936
1541
  }
937
1542
  return expanded;
938
1543
  }
1544
+ function tracePolygonPath(ctx, x, y, points) {
1545
+ for (let i = 0; i < points.length; i += 2) {
1546
+ const px = x + points[i];
1547
+ const py = y + points[i + 1];
1548
+ if (i === 0) ctx.moveTo(px, py);
1549
+ else ctx.lineTo(px, py);
1550
+ }
1551
+ ctx.closePath();
1552
+ }
939
1553
  function canvasToUrl(canvas) {
940
1554
  if (typeof canvas.toBlob === "function") {
941
1555
  return new Promise((resolve) => {
@@ -979,6 +1593,176 @@ function drawImageUvSample(ctx, img, rect, x, y, width, height, atlasScale) {
979
1593
  setCssTransform(ctx, atlasScale);
980
1594
  ctx.drawImage(img, sx, sy, sw, sh, x, y, width, height);
981
1595
  }
1596
+ function traceOffsetPolygonPath(ctx, x, y, points, offsetX, offsetY) {
1597
+ for (let i = 0; i < points.length; i += 2) {
1598
+ const px = x + points[i] + offsetX;
1599
+ const py = y + points[i + 1] + offsetY;
1600
+ if (i === 0) ctx.moveTo(px, py);
1601
+ else ctx.lineTo(px, py);
1602
+ }
1603
+ ctx.closePath();
1604
+ }
1605
+ function drawTexturedAtlasEntry(ctx, entry, srcImg, atlasScale, offsetX = 0, offsetY = 0) {
1606
+ if (entry.textureTriangles?.length) {
1607
+ const imgW = srcImg.naturalWidth || srcImg.width || 1;
1608
+ const imgH = srcImg.naturalHeight || srcImg.height || 1;
1609
+ for (const triangle of entry.textureTriangles) {
1610
+ const clipPts = expandClipPoints(triangle.screenPts, TEXTURE_TRIANGLE_BLEED);
1611
+ ctx.save();
1612
+ setCssTransform(ctx, atlasScale);
1613
+ ctx.beginPath();
1614
+ traceOffsetPolygonPath(ctx, entry.x, entry.y, clipPts, offsetX, offsetY);
1615
+ ctx.clip();
1616
+ if (triangle.uvAffine) {
1617
+ setCssTransform(
1618
+ ctx,
1619
+ atlasScale,
1620
+ triangle.uvAffine.a / imgW,
1621
+ triangle.uvAffine.c / imgW,
1622
+ triangle.uvAffine.b / imgH,
1623
+ triangle.uvAffine.d / imgH,
1624
+ entry.x + triangle.uvAffine.e + offsetX,
1625
+ entry.y + triangle.uvAffine.f + offsetY
1626
+ );
1627
+ ctx.drawImage(srcImg, 0, 0);
1628
+ } else if (triangle.uvSampleRect) {
1629
+ drawImageUvSample(
1630
+ ctx,
1631
+ srcImg,
1632
+ triangle.uvSampleRect,
1633
+ entry.x + offsetX,
1634
+ entry.y + offsetY,
1635
+ entry.canvasW,
1636
+ entry.canvasH,
1637
+ atlasScale
1638
+ );
1639
+ }
1640
+ ctx.restore();
1641
+ }
1642
+ } else if (entry.uvAffine) {
1643
+ const imgW = srcImg.naturalWidth || srcImg.width || 1;
1644
+ const imgH = srcImg.naturalHeight || srcImg.height || 1;
1645
+ setCssTransform(
1646
+ ctx,
1647
+ atlasScale,
1648
+ entry.uvAffine.a / imgW,
1649
+ entry.uvAffine.c / imgW,
1650
+ entry.uvAffine.b / imgH,
1651
+ entry.uvAffine.d / imgH,
1652
+ entry.x + entry.uvAffine.e + offsetX,
1653
+ entry.y + entry.uvAffine.f + offsetY
1654
+ );
1655
+ ctx.drawImage(srcImg, 0, 0);
1656
+ } else if (entry.uvSampleRect) {
1657
+ drawImageUvSample(
1658
+ ctx,
1659
+ srcImg,
1660
+ entry.uvSampleRect,
1661
+ entry.x + offsetX,
1662
+ entry.y + offsetY,
1663
+ entry.canvasW,
1664
+ entry.canvasH,
1665
+ atlasScale
1666
+ );
1667
+ } else {
1668
+ drawImageCover(
1669
+ ctx,
1670
+ srcImg,
1671
+ entry.x + offsetX,
1672
+ entry.y + offsetY,
1673
+ entry.canvasW,
1674
+ entry.canvasH,
1675
+ atlasScale
1676
+ );
1677
+ }
1678
+ }
1679
+ function distanceToSegment(px, py, ax, ay, bx, by) {
1680
+ const dx = bx - ax;
1681
+ const dy = by - ay;
1682
+ const lenSq = dx * dx + dy * dy;
1683
+ if (lenSq <= BASIS_EPS) return Math.hypot(px - ax, py - ay);
1684
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
1685
+ return Math.hypot(px - (ax + dx * t), py - (ay + dy * t));
1686
+ }
1687
+ function distanceToPolygonEdges(px, py, points, edgeIndices) {
1688
+ let best = Infinity;
1689
+ const count = points.length / 2;
1690
+ for (const edgeIndex of edgeIndices) {
1691
+ if (edgeIndex < 0 || edgeIndex >= count) continue;
1692
+ const i = edgeIndex * 2;
1693
+ const next = (edgeIndex + 1) % count * 2;
1694
+ best = Math.min(
1695
+ best,
1696
+ distanceToSegment(px, py, points[i], points[i + 1], points[next], points[next + 1])
1697
+ );
1698
+ }
1699
+ return best;
1700
+ }
1701
+ function nearestOpaquePixelOffset(data, width, height, x, y, radius) {
1702
+ const minX = Math.max(0, x - radius);
1703
+ const maxX = Math.min(width - 1, x + radius);
1704
+ const minY = Math.max(0, y - radius);
1705
+ const maxY = Math.min(height - 1, y + radius);
1706
+ let bestOffset = null;
1707
+ let bestDistanceSq = Infinity;
1708
+ for (let yy = minY; yy <= maxY; yy++) {
1709
+ for (let xx = minX; xx <= maxX; xx++) {
1710
+ if (xx === x && yy === y) continue;
1711
+ const dx = xx - x;
1712
+ const dy = yy - y;
1713
+ const distanceSq = dx * dx + dy * dy;
1714
+ if (distanceSq > radius * radius || distanceSq >= bestDistanceSq) continue;
1715
+ const offset = (yy * width + xx) * 4;
1716
+ if (data[offset + 3] < TEXTURE_EDGE_REPAIR_SOURCE_ALPHA_MIN) continue;
1717
+ bestOffset = offset;
1718
+ bestDistanceSq = distanceSq;
1719
+ }
1720
+ }
1721
+ return bestOffset;
1722
+ }
1723
+ function repairTextureEdgeAlpha(ctx, entry, atlasScale) {
1724
+ if (!entry.textureEdgeRepair || !entry.texture) return;
1725
+ if (!entry.textureEdgeRepairEdges || entry.textureEdgeRepairEdges.size === 0) return;
1726
+ const canvas = ctx.canvas;
1727
+ if (!canvas) return;
1728
+ const pixelX = Math.max(0, Math.floor(entry.x * atlasScale));
1729
+ const pixelY = Math.max(0, Math.floor(entry.y * atlasScale));
1730
+ const pixelW = Math.max(1, Math.min(canvas.width - pixelX, Math.ceil(entry.canvasW * atlasScale)));
1731
+ const pixelH = Math.max(1, Math.min(canvas.height - pixelY, Math.ceil(entry.canvasH * atlasScale)));
1732
+ if (pixelW <= 0 || pixelH <= 0) return;
1733
+ let imageData;
1734
+ try {
1735
+ imageData = ctx.getImageData(pixelX, pixelY, pixelW, pixelH);
1736
+ } catch {
1737
+ return;
1738
+ }
1739
+ const data = imageData.data;
1740
+ const source = new Uint8ClampedArray(data);
1741
+ const radius = Math.max(TEXTURE_EDGE_REPAIR_RADIUS, TEXTURE_EDGE_REPAIR_RADIUS / atlasScale);
1742
+ const sourceRadius = Math.max(2, Math.ceil(radius * atlasScale) + 1);
1743
+ let changed = false;
1744
+ for (let y = 0; y < pixelH; y++) {
1745
+ for (let x = 0; x < pixelW; x++) {
1746
+ const offset = (y * pixelW + x) * 4;
1747
+ const alpha = data[offset + 3];
1748
+ if (alpha < TEXTURE_EDGE_REPAIR_ALPHA_MIN || alpha === 255) continue;
1749
+ const localX = (pixelX + x + 0.5) / atlasScale - entry.x;
1750
+ const localY = (pixelY + y + 0.5) / atlasScale - entry.y;
1751
+ if (distanceToPolygonEdges(localX, localY, entry.screenPts, entry.textureEdgeRepairEdges) > radius) {
1752
+ continue;
1753
+ }
1754
+ const sourceOffset = nearestOpaquePixelOffset(source, pixelW, pixelH, x, y, sourceRadius);
1755
+ if (sourceOffset === null) continue;
1756
+ data[offset] = source[sourceOffset];
1757
+ data[offset + 1] = source[sourceOffset + 1];
1758
+ data[offset + 2] = source[sourceOffset + 2];
1759
+ data[offset + 3] = 255;
1760
+ changed = true;
1761
+ }
1762
+ }
1763
+ if (!changed) return;
1764
+ ctx.putImageData(imageData, pixelX, pixelY);
1765
+ }
982
1766
  function computeTextureAtlasPlan(polygon, index, options = {}) {
983
1767
  const { vertices, texture, uvs } = polygon;
984
1768
  if (!vertices || vertices.length < 3) return null;
@@ -1025,18 +1809,20 @@ function computeTextureAtlasPlan(polygon, index, options = {}) {
1025
1809
  if (y < yMin) yMin = y;
1026
1810
  if (y > yMax) yMax = y;
1027
1811
  }
1028
- const shiftX = -xMin;
1029
- const shiftY = -yMin;
1030
1812
  const w = xMax - xMin;
1031
1813
  const h = yMax - yMin;
1032
1814
  if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
1815
+ const textureEdgeRepairEdges = texture && options.textureEdgeRepairEdges?.size ? options.textureEdgeRepairEdges : null;
1816
+ const textureEdgeRepair = Boolean(texture && textureEdgeRepairEdges);
1817
+ const shiftX = -xMin;
1818
+ const shiftY = -yMin;
1033
1819
  const screenPts = [];
1034
1820
  for (let i = 0; i < local2D.length; i++) {
1035
1821
  const [x, y] = local2D[i];
1036
- const sx = x + shiftX;
1037
- const sy = y + shiftY;
1038
- screenPts.push(sx, sy);
1822
+ screenPts.push(x + shiftX, y + shiftY);
1039
1823
  }
1824
+ const canvasW = Math.max(1, Math.ceil(w));
1825
+ const canvasH = Math.max(1, Math.ceil(h));
1040
1826
  const tx = p0[0] - shiftX * xAxis[0] - shiftY * yAxis[0];
1041
1827
  const ty = p0[1] - shiftX * xAxis[1] - shiftY * yAxis[1];
1042
1828
  const tz = p0[2] - shiftX * xAxis[2] - shiftY * yAxis[2];
@@ -1058,6 +1844,44 @@ function computeTextureAtlasPlan(polygon, index, options = {}) {
1058
1844
  tz,
1059
1845
  1
1060
1846
  ].join(",");
1847
+ const canonicalMatrix = [
1848
+ xAxis[0] * canvasW,
1849
+ xAxis[1] * canvasW,
1850
+ xAxis[2] * canvasW,
1851
+ 0,
1852
+ yAxis[0] * canvasH,
1853
+ yAxis[1] * canvasH,
1854
+ yAxis[2] * canvasH,
1855
+ 0,
1856
+ nx,
1857
+ ny,
1858
+ nz,
1859
+ 0,
1860
+ tx,
1861
+ ty,
1862
+ tz,
1863
+ 1
1864
+ ].join(",");
1865
+ const atlasMatrix = [
1866
+ xAxis[0] * canvasW / ATLAS_CANONICAL_SIZE_EXPLICIT,
1867
+ xAxis[1] * canvasW / ATLAS_CANONICAL_SIZE_EXPLICIT,
1868
+ xAxis[2] * canvasW / ATLAS_CANONICAL_SIZE_EXPLICIT,
1869
+ 0,
1870
+ yAxis[0] * canvasH / ATLAS_CANONICAL_SIZE_EXPLICIT,
1871
+ yAxis[1] * canvasH / ATLAS_CANONICAL_SIZE_EXPLICIT,
1872
+ yAxis[2] * canvasH / ATLAS_CANONICAL_SIZE_EXPLICIT,
1873
+ 0,
1874
+ nx,
1875
+ ny,
1876
+ nz,
1877
+ 0,
1878
+ tx,
1879
+ ty,
1880
+ tz,
1881
+ 1
1882
+ ].join(",");
1883
+ const normal = [nx, ny, nz];
1884
+ const projectiveMatrix = !texture && vertices.length === 4 ? computeProjectiveQuadMatrix(screenPts, xAxis, yAxis, normal, tx, ty, tz) : null;
1061
1885
  const directionalCfg = options.directionalLight;
1062
1886
  const ambientCfg = options.ambientLight;
1063
1887
  const lightDir = directionalCfg?.direction ?? DEFAULT_LIGHT_DIR;
@@ -1089,13 +1913,18 @@ function computeTextureAtlasPlan(polygon, index, options = {}) {
1089
1913
  tileSize: tile,
1090
1914
  layerElevation: elev,
1091
1915
  matrix,
1092
- canvasW: Math.max(1, Math.ceil(w)),
1093
- canvasH: Math.max(1, Math.ceil(h)),
1916
+ canonicalMatrix,
1917
+ atlasMatrix,
1918
+ projectiveMatrix,
1919
+ canvasW,
1920
+ canvasH,
1094
1921
  screenPts,
1095
1922
  uvAffine,
1096
1923
  uvSampleRect,
1097
1924
  textureTriangles,
1098
- normal: [nx, ny, nz],
1925
+ textureEdgeRepairEdges,
1926
+ textureEdgeRepair,
1927
+ normal,
1099
1928
  textureTint,
1100
1929
  shadedColor
1101
1930
  };
@@ -1179,7 +2008,10 @@ async function buildAtlasPage(page, textureLighting, doc, atlasScale) {
1179
2008
  const canvas = doc.createElement("canvas");
1180
2009
  canvas.width = Math.max(1, Math.ceil(page.width * atlasScale));
1181
2010
  canvas.height = Math.max(1, Math.ceil(page.height * atlasScale));
1182
- const ctx = canvas.getContext("2d");
2011
+ const needsReadback = page.entries.some(
2012
+ (entry) => entry.textureEdgeRepair && entry.texture && entry.textureEdgeRepairEdges && entry.textureEdgeRepairEdges.size > 0
2013
+ );
2014
+ const ctx = canvas.getContext("2d", needsReadback ? { willReadFrequently: true } : void 0);
1183
2015
  if (!ctx) return { width: page.width, height: page.height, url: null };
1184
2016
  const uniqueTextures = Array.from(new Set(
1185
2017
  page.entries.flatMap((entry) => entry.texture ? [entry.texture] : [])
@@ -1190,97 +2022,36 @@ async function buildAtlasPage(page, textureLighting, doc, atlasScale) {
1190
2022
  }));
1191
2023
  for (const entry of page.entries) {
1192
2024
  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
2025
  if (!entry.texture) {
2026
+ ctx.save();
1205
2027
  setCssTransform(ctx, atlasScale);
2028
+ ctx.beginPath();
2029
+ tracePolygonPath(ctx, entry.x, entry.y, entry.screenPts);
2030
+ ctx.clip();
1206
2031
  ctx.fillStyle = textureLighting === "dynamic" ? entry.polygon.color ?? "#cccccc" : entry.shadedColor;
1207
2032
  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
- }
2033
+ ctx.restore();
2034
+ continue;
2035
+ }
2036
+ if (srcImg) {
2037
+ ctx.save();
2038
+ setCssTransform(ctx, atlasScale);
2039
+ ctx.beginPath();
2040
+ tracePolygonPath(ctx, entry.x, entry.y, entry.screenPts);
2041
+ ctx.clip();
2042
+ drawTexturedAtlasEntry(ctx, entry, srcImg, atlasScale);
2043
+ ctx.restore();
1279
2044
  }
1280
2045
  if (entry.texture && textureLighting === "baked") {
2046
+ ctx.save();
2047
+ setCssTransform(ctx, atlasScale);
2048
+ ctx.beginPath();
2049
+ tracePolygonPath(ctx, entry.x, entry.y, entry.screenPts);
2050
+ ctx.clip();
1281
2051
  applyTextureTint(ctx, entry.x, entry.y, entry.canvasW, entry.canvasH, entry.textureTint, atlasScale);
2052
+ ctx.restore();
1282
2053
  }
1283
- ctx.restore();
2054
+ repairTextureEdgeAlpha(ctx, entry, atlasScale);
1284
2055
  }
1285
2056
  const url = await canvasToUrl(canvas);
1286
2057
  canvas.width = 1;
@@ -1299,21 +2070,32 @@ async function buildAtlasPages(pages, textureLighting, doc, atlasScale, isCancel
1299
2070
  }
1300
2071
  return built;
1301
2072
  }
1302
- function useTextureAtlas(plans, textureLighting, atlasScaleInput) {
1303
- const useBorderShape = textureLighting !== "dynamic" && borderShapeSupported();
2073
+ function useTextureAtlas(plans, textureLighting, textureQualityInput, strategies) {
2074
+ const disableB = strategies?.disable?.includes("b") ?? false;
2075
+ const disableI = strategies?.disable?.includes("i") ?? false;
2076
+ const disableU = strategies?.disable?.includes("u") ?? false;
2077
+ const useFullRectSolid = !disableB;
2078
+ const useProjectiveQuad = useFullRectSolid;
2079
+ const useStableTriangle = !disableU && solidTriangleSupported();
2080
+ const useBorderShape = !disableI && textureLighting !== "dynamic" && borderShapeSupported();
1304
2081
  const atlasPlans = useMemo5(
1305
2082
  () => plans.map((plan) => {
1306
2083
  if (!plan) return plan;
1307
2084
  if (plan.texture) return plan;
1308
- if (isSolidTrianglePlan(plan)) return null;
1309
- if (textureLighting !== "dynamic" && (useBorderShape || isFullRectSolid(plan))) return null;
2085
+ if (useStableTriangle && isSolidTrianglePlan(plan)) return null;
2086
+ const fullRect = isFullRectSolid(plan);
2087
+ if (useFullRectSolid && fullRect || useProjectiveQuad && isProjectiveQuadPlan(plan) || textureLighting !== "dynamic" && useBorderShape && (!fullRect || disableB)) return null;
1310
2088
  return plan;
1311
2089
  }),
1312
- [plans, textureLighting, useBorderShape]
2090
+ [plans, textureLighting, useFullRectSolid, useProjectiveQuad, useStableTriangle, useBorderShape]
1313
2091
  );
1314
2092
  const { packed, atlasScale } = useMemo5(
1315
- () => packTextureAtlasPlansWithScale(atlasPlans, atlasScaleInput),
1316
- [atlasPlans, atlasScaleInput]
2093
+ () => packTextureAtlasPlansWithScale(
2094
+ atlasPlans,
2095
+ textureQualityInput,
2096
+ typeof document !== "undefined" ? document : null
2097
+ ),
2098
+ [atlasPlans, textureQualityInput]
1317
2099
  );
1318
2100
  const [pages, setPages] = useState(
1319
2101
  () => packed.pages.map((page) => ({ width: page.width, height: page.height, url: null }))
@@ -1353,54 +2135,103 @@ function useTextureAtlas(plans, textureLighting, atlasScaleInput) {
1353
2135
  }
1354
2136
  function TextureBorderShapePoly({
1355
2137
  entry,
2138
+ solidPaintDefaults,
1356
2139
  className,
1357
2140
  style: styleProp,
1358
2141
  domAttrs,
1359
2142
  domEventHandlers,
1360
- pointerEvents = "auto"
2143
+ pointerEvents = "auto",
2144
+ disabledStrategies
1361
2145
  }) {
1362
2146
  const fullRect = isFullRectSolid(entry);
1363
- const borderShape = fullRect ? null : cssBorderShapeForPlan(entry);
2147
+ const bDisabled = disabledStrategies?.has("b") ?? false;
2148
+ const useIForFullRect = bDisabled && borderShapeSupported();
2149
+ const borderShape = !fullRect || useIForFullRect ? cssBorderShapeForPlan(entry) : null;
2150
+ const useDefaultPaint = entry.shadedColor === solidPaintDefaults?.paintColor;
1364
2151
  const setElementRef = useCallback3((el) => {
1365
2152
  if (!el) return;
1366
2153
  if (borderShape) el.style.setProperty("border-shape", borderShape);
1367
2154
  else el.style.removeProperty("border-shape");
2155
+ orderBrushInlineStyle(el);
1368
2156
  }, [borderShape]);
2157
+ const transform = formatMatrix3d(borderShape ? formatBorderShapeMatrix(entry) : formatSolidQuadMatrix(entry));
1369
2158
  const style = fullRect ? {
1370
- width: entry.canvasW,
1371
- height: entry.canvasH,
1372
- transform: formatMatrix3d(entry.matrix),
1373
- background: entry.shadedColor,
2159
+ transform,
2160
+ color: useDefaultPaint ? void 0 : entry.shadedColor,
1374
2161
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
1375
2162
  ...styleProp
1376
2163
  } : {
1377
- width: entry.canvasW,
1378
- height: entry.canvasH,
1379
- transform: formatMatrix3d(entry.matrix),
1380
- color: entry.shadedColor,
2164
+ transform,
2165
+ color: useDefaultPaint ? void 0 : entry.shadedColor,
2166
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
2167
+ ...styleProp
2168
+ };
2169
+ const dataAttrs = entry.polygon.data ? Object.fromEntries(
2170
+ Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
2171
+ ) : {};
2172
+ const elementClassName = className?.trim() || void 0;
2173
+ if (fullRect && !useIForFullRect) {
2174
+ return /* @__PURE__ */ jsx3(
2175
+ "b",
2176
+ {
2177
+ className: elementClassName,
2178
+ style,
2179
+ ...domEventHandlers,
2180
+ ...dataAttrs,
2181
+ ...domAttrs
2182
+ }
2183
+ );
2184
+ }
2185
+ return /* @__PURE__ */ jsx3(
2186
+ "i",
2187
+ {
2188
+ ref: setElementRef,
2189
+ className: elementClassName,
2190
+ style,
2191
+ ...domEventHandlers,
2192
+ ...dataAttrs,
2193
+ ...domAttrs
2194
+ }
2195
+ );
2196
+ }
2197
+ function TextureProjectiveSolidPoly({
2198
+ entry,
2199
+ textureLighting,
2200
+ solidPaintDefaults,
2201
+ className,
2202
+ style: styleProp,
2203
+ domAttrs,
2204
+ domEventHandlers,
2205
+ pointerEvents = "auto"
2206
+ }) {
2207
+ const dynamic = textureLighting === "dynamic";
2208
+ const base = parseHex(entry.polygon.color ?? "#cccccc");
2209
+ const useDefaultDynamicColor = dynamic && rgbKey(base) === solidPaintDefaults?.dynamicColorKey;
2210
+ const style = {
2211
+ transform: formatMatrix3d(entry.projectiveMatrix),
2212
+ color: dynamic || entry.shadedColor === solidPaintDefaults?.paintColor ? void 0 : entry.shadedColor,
1381
2213
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
2214
+ ...dynamic && !useDefaultDynamicColor ? {
2215
+ ["--pnx"]: entry.normal[0].toFixed(4),
2216
+ ["--pny"]: entry.normal[1].toFixed(4),
2217
+ ["--pnz"]: entry.normal[2].toFixed(4),
2218
+ ["--psr"]: (base.r / 255).toFixed(4),
2219
+ ["--psg"]: (base.g / 255).toFixed(4),
2220
+ ["--psb"]: (base.b / 255).toFixed(4)
2221
+ } : dynamic ? {
2222
+ ["--pnx"]: entry.normal[0].toFixed(4),
2223
+ ["--pny"]: entry.normal[1].toFixed(4),
2224
+ ["--pnz"]: entry.normal[2].toFixed(4)
2225
+ } : null,
1382
2226
  ...styleProp
1383
2227
  };
1384
2228
  const dataAttrs = entry.polygon.data ? Object.fromEntries(
1385
2229
  Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
1386
2230
  ) : {};
1387
2231
  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
2232
  return /* @__PURE__ */ jsx3(
1401
- "i",
2233
+ "b",
1402
2234
  {
1403
- ref: setElementRef,
1404
2235
  className: elementClassName,
1405
2236
  style,
1406
2237
  ...domEventHandlers,
@@ -1412,13 +2243,14 @@ function TextureBorderShapePoly({
1412
2243
  function TextureTrianglePoly({
1413
2244
  entry,
1414
2245
  textureLighting,
2246
+ solidPaintDefaults,
1415
2247
  className,
1416
2248
  style: styleProp,
1417
2249
  domAttrs,
1418
2250
  domEventHandlers,
1419
2251
  pointerEvents = "auto"
1420
2252
  }) {
1421
- const triangleStyle = solidTriangleStyle(entry, textureLighting, pointerEvents);
2253
+ const triangleStyle = solidTriangleStyle(entry, textureLighting, pointerEvents, solidPaintDefaults);
1422
2254
  if (!triangleStyle) return null;
1423
2255
  const dataAttrs = entry.polygon.data ? Object.fromEntries(
1424
2256
  Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
@@ -1446,16 +2278,20 @@ function TextureAtlasPoly({
1446
2278
  pointerEvents = "auto"
1447
2279
  }) {
1448
2280
  const dynamic = textureLighting === "dynamic";
2281
+ const atlasCanonicalSize = entry.atlasCanonicalSize ?? ATLAS_CANONICAL_SIZE_EXPLICIT;
2282
+ const atlasWidth = entry.canvasW || 1;
2283
+ const atlasHeight = entry.canvasH || 1;
2284
+ const atlasPosition = page ? `${formatCssLength(-entry.x / atlasWidth * atlasCanonicalSize)} ${formatCssLength(-entry.y / atlasHeight * atlasCanonicalSize)}` : void 0;
2285
+ const atlasSize = page ? `${formatCssLength(page.width / atlasWidth * atlasCanonicalSize)} ${formatCssLength(page.height / atlasHeight * atlasCanonicalSize)}` : void 0;
1449
2286
  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;
2287
+ const background = !dynamic && page?.url ? `url(${page.url}) ${atlasPosition} / ${atlasSize} no-repeat` : void 0;
1451
2288
  const style = {
1452
- width: entry.canvasW,
1453
- height: entry.canvasH,
1454
- transform: formatMatrix3d(entry.matrix),
2289
+ transform: formatMatrix3d(entry.atlasMatrix),
2290
+ ["--polycss-atlas-size"]: `${atlasCanonicalSize}px`,
1455
2291
  background,
1456
2292
  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,
2293
+ backgroundPosition: dynamic ? atlasPosition : void 0,
2294
+ backgroundSize: dynamic ? atlasSize : void 0,
1459
2295
  ...dynamic ? {
1460
2296
  ["--pnx"]: entry.normal[0].toFixed(4),
1461
2297
  ["--pny"]: entry.normal[1].toFixed(4),
@@ -1466,12 +2302,12 @@ function TextureAtlasPoly({
1466
2302
  // the polygon don't get painted with the tint.
1467
2303
  maskImage: dynamicMask,
1468
2304
  maskMode: "alpha",
1469
- maskPosition: `-${entry.x}px -${entry.y}px`,
1470
- maskSize: page ? `${page.width}px ${page.height}px` : void 0,
2305
+ maskPosition: atlasPosition,
2306
+ maskSize: atlasSize,
1471
2307
  maskRepeat: "no-repeat",
1472
2308
  WebkitMaskImage: dynamicMask,
1473
- WebkitMaskPosition: `-${entry.x}px -${entry.y}px`,
1474
- WebkitMaskSize: page ? `${page.width}px ${page.height}px` : void 0,
2309
+ WebkitMaskPosition: atlasPosition,
2310
+ WebkitMaskSize: atlasSize,
1475
2311
  WebkitMaskRepeat: "no-repeat"
1476
2312
  } : null,
1477
2313
  opacity: page?.url ? void 0 : 0,
@@ -1502,7 +2338,7 @@ function usePolySceneContext2() {
1502
2338
  }
1503
2339
 
1504
2340
  // src/scene/PolyScene.tsx
1505
- import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
2341
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
1506
2342
  function PolySceneInner({
1507
2343
  polygons: polygonsProp,
1508
2344
  centerPolygons: centerPolygonsProp,
@@ -1513,8 +2349,10 @@ function PolySceneInner({
1513
2349
  directionalLight,
1514
2350
  ambientLight,
1515
2351
  textureLighting = "baked",
1516
- atlasScale,
2352
+ textureQuality,
2353
+ strategies,
1517
2354
  autoCenter = false,
2355
+ shadow,
1518
2356
  className,
1519
2357
  style,
1520
2358
  children,
@@ -1558,14 +2396,6 @@ function PolySceneInner({
1558
2396
  { directionalLight }
1559
2397
  );
1560
2398
  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
2399
  const directionalForAtlas = textureLighting === "dynamic" ? void 0 : directionalLight;
1570
2400
  const ambientForAtlas = textureLighting === "dynamic" ? void 0 : ambientLight;
1571
2401
  const polyContext = useMemo6(() => {
@@ -1578,11 +2408,43 @@ function PolySceneInner({
1578
2408
  textureLighting
1579
2409
  };
1580
2410
  }, [directionalForAtlas, ambientForAtlas, textureLighting]);
2411
+ const autoCenterOffset = useMemo6(() => {
2412
+ if (!autoCenter) return [0, 0, 0];
2413
+ return [
2414
+ (sceneBbox.min[0] + sceneBbox.max[0]) / 2,
2415
+ (sceneBbox.min[1] + sceneBbox.max[1]) / 2,
2416
+ (sceneBbox.min[2] + sceneBbox.max[2]) / 2
2417
+ ];
2418
+ }, [autoCenter, sceneBbox]);
2419
+ useEffect3(() => {
2420
+ store.setAutoCenterOffset(autoCenterOffset);
2421
+ }, [store, autoCenterOffset]);
2422
+ const sceneStyle = useMemo6(() => {
2423
+ const s = cameraState;
2424
+ const [ox, oy, oz] = autoCenterOffset;
2425
+ const tileSize = BASE_TILE2;
2426
+ const wx = s.target[0] + ox;
2427
+ const wy = s.target[1] + oy;
2428
+ const wz = s.target[2] + oz;
2429
+ const cssX = wy * tileSize;
2430
+ const cssY = wx * tileSize;
2431
+ const cssZ = wz * tileSize;
2432
+ const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
2433
+ const transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
2434
+ return { transform };
2435
+ }, [cameraState, autoCenterOffset]);
2436
+ const computedClassName = `polycss-scene${className ? ` ${className}` : ""}`;
1581
2437
  const textureAtlasPlans = useMemo6(
1582
- () => polygons.map((p, i) => computeTextureAtlasPlan(p, i, polyContext)),
2438
+ () => {
2439
+ const repairEdges = buildTextureEdgeRepairSets(polygons);
2440
+ return polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
2441
+ ...polyContext,
2442
+ textureEdgeRepairEdges: repairEdges[i]
2443
+ }));
2444
+ },
1583
2445
  [polygons, polyContext]
1584
2446
  );
1585
- const textureAtlas = useTextureAtlas(textureAtlasPlans, textureLighting, atlasScale);
2447
+ const textureAtlas = useTextureAtlas(textureAtlasPlans, textureLighting, textureQuality, strategies);
1586
2448
  const dynamicLightVars = useMemo6(() => {
1587
2449
  if (textureLighting !== "dynamic") return null;
1588
2450
  const dir = directionalLight?.direction ?? [0.4, -0.7, 0.59];
@@ -1593,6 +2455,8 @@ function PolySceneInner({
1593
2455
  const lightIntensity = directionalLight?.intensity ?? 1;
1594
2456
  const ambientIntensity = ambientLight?.intensity ?? 0.4;
1595
2457
  const ch = (n) => (n / 255).toFixed(4);
2458
+ const rawClz = lz;
2459
+ const clz = Math.sign(rawClz || 1) * Math.max(Math.abs(rawClz), 0.01);
1596
2460
  return {
1597
2461
  ["--plx"]: lx.toFixed(4),
1598
2462
  ["--ply"]: ly.toFixed(4),
@@ -1604,16 +2468,52 @@ function PolySceneInner({
1604
2468
  ["--par"]: ch(ambRgb[0]),
1605
2469
  ["--pag"]: ch(ambRgb[1]),
1606
2470
  ["--pab"]: ch(ambRgb[2]),
1607
- ["--pai"]: ambientIntensity.toFixed(4)
2471
+ ["--pai"]: ambientIntensity.toFixed(4),
2472
+ ["--clx"]: lx.toFixed(4),
2473
+ ["--cly"]: ly.toFixed(4),
2474
+ ["--clz"]: clz.toFixed(4)
1608
2475
  };
1609
2476
  }, [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]);
2477
+ const shadowCastersRef = useRef3(/* @__PURE__ */ new Map());
2478
+ const registerShadowCaster = useCallback4((meshId, meshPolygons) => {
2479
+ if (meshPolygons === null) {
2480
+ shadowCastersRef.current.delete(meshId);
2481
+ } else {
2482
+ shadowCastersRef.current.set(meshId, meshPolygons);
2483
+ }
2484
+ const el = sceneElRef.current;
2485
+ if (!el) return;
2486
+ if (textureLighting !== "dynamic") {
2487
+ el.style.removeProperty("--shadow-ground-cssz");
2488
+ return;
2489
+ }
2490
+ let minWorldZ = Infinity;
2491
+ for (const polys of shadowCastersRef.current.values()) {
2492
+ for (const poly of polys) {
2493
+ for (const v of poly.vertices) {
2494
+ if (v[2] < minWorldZ) minWorldZ = v[2];
2495
+ }
2496
+ }
2497
+ }
2498
+ if (!Number.isFinite(minWorldZ)) {
2499
+ el.style.removeProperty("--shadow-ground-cssz");
2500
+ return;
2501
+ }
2502
+ const lift = shadow?.lift ?? 0.05;
2503
+ const groundCssZ = (minWorldZ + lift) * BASE_TILE2;
2504
+ el.style.setProperty("--shadow-ground-cssz", groundCssZ.toFixed(3));
2505
+ }, [sceneElRef, textureLighting, shadow]);
2506
+ useEffect3(() => {
2507
+ const el = sceneElRef.current;
2508
+ if (!el) return;
2509
+ if (textureLighting !== "dynamic") {
2510
+ el.style.removeProperty("--shadow-ground-cssz");
2511
+ }
2512
+ }, [textureLighting, sceneElRef]);
2513
+ const disabledStrategies = useMemo6(
2514
+ () => strategies?.disable?.length ? new Set(strategies.disable) : void 0,
2515
+ [strategies]
2516
+ );
1617
2517
  const polyChildren = textureAtlas.entries.map((entry, index) => {
1618
2518
  if (entry) {
1619
2519
  return /* @__PURE__ */ jsx4(
@@ -1628,13 +2528,21 @@ function PolySceneInner({
1628
2528
  }
1629
2529
  const plan = textureAtlasPlans[index];
1630
2530
  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);
2531
+ const useU = !disabledStrategies?.has("u");
2532
+ const useProjectiveSolid = !disabledStrategies?.has("b");
2533
+ if (useU && isSolidTrianglePlan(plan)) {
2534
+ return /* @__PURE__ */ jsx4(TextureTrianglePoly, { entry: plan, textureLighting }, plan.index);
2535
+ }
2536
+ if (useProjectiveSolid && isProjectiveQuadPlan(plan)) {
2537
+ return /* @__PURE__ */ jsx4(TextureProjectiveSolidPoly, { entry: plan, textureLighting }, plan.index);
2538
+ }
2539
+ return /* @__PURE__ */ jsx4(TextureBorderShapePoly, { entry: plan, disabledStrategies }, plan.index);
1632
2540
  });
1633
2541
  const sceneCtxValue = useMemo6(
1634
- () => ({ textureLighting, directionalLight, ambientLight }),
1635
- [textureLighting, directionalLight, ambientLight]
2542
+ () => ({ textureLighting, directionalLight, ambientLight, shadow, registerShadowCaster }),
2543
+ [textureLighting, directionalLight, ambientLight, shadow, registerShadowCaster]
1636
2544
  );
1637
- return /* @__PURE__ */ jsx4(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ jsx4(
2545
+ return /* @__PURE__ */ jsx4(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ jsxs(
1638
2546
  "div",
1639
2547
  {
1640
2548
  ref: localSceneRef,
@@ -1648,13 +2556,10 @@ function PolySceneInner({
1648
2556
  // No more --polycss-rows / --polycss-cols — CSS Grid was dropped
1649
2557
  // in Phase 4 (per §Design.4a).
1650
2558
  },
1651
- children: autoCenterTransform ? /* @__PURE__ */ jsxs("div", { className: "polycss-offset", style: { ["--offset-transform"]: autoCenterTransform }, children: [
1652
- polyChildren,
1653
- children
1654
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2559
+ children: [
1655
2560
  polyChildren,
1656
2561
  children
1657
- ] })
2562
+ ]
1658
2563
  }
1659
2564
  ) });
1660
2565
  }
@@ -1671,7 +2576,7 @@ import {
1671
2576
  useRef as useRef5,
1672
2577
  useState as useState3
1673
2578
  } from "react";
1674
- import { computeSceneBbox, inverseRotateVec3 } from "@layoutit/polycss-core";
2579
+ import { computeSceneBbox, findOverlappingPolygonDuplicates, inverseRotateVec3, parseHexColor as parseHexColor2 } from "@layoutit/polycss-core";
1675
2580
 
1676
2581
  // src/scene/useMesh.ts
1677
2582
  import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
@@ -1802,7 +2707,17 @@ function findMeshUnderPoint(clientX, clientY, filter) {
1802
2707
  }
1803
2708
 
1804
2709
  // src/scene/PolyMesh.tsx
1805
- import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
2710
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
2711
+ function solidPaintVars(defaults) {
2712
+ const out = {};
2713
+ if (defaults.paintColor) out["--polycss-paint"] = defaults.paintColor;
2714
+ if (defaults.dynamicColor) {
2715
+ out["--psr"] = (defaults.dynamicColor.r / 255).toFixed(4);
2716
+ out["--psg"] = (defaults.dynamicColor.g / 255).toFixed(4);
2717
+ out["--psb"] = (defaults.dynamicColor.b / 255).toFixed(4);
2718
+ }
2719
+ return Object.keys(out).length > 0 ? out : null;
2720
+ }
1806
2721
  function buildTransform(position, scale, rotation) {
1807
2722
  const parts = [];
1808
2723
  if (position) {
@@ -1848,7 +2763,8 @@ var PolyMesh = forwardRef(function PolyMesh2({
1848
2763
  polygons: polygonsProp,
1849
2764
  autoCenter,
1850
2765
  textureLighting,
1851
- atlasScale,
2766
+ textureQuality,
2767
+ castShadow,
1852
2768
  children,
1853
2769
  fallback,
1854
2770
  errorFallback,
@@ -1876,12 +2792,43 @@ var PolyMesh = forwardRef(function PolyMesh2({
1876
2792
  return { ...parseOptions ?? {}, ...mtl ? { mtlUrl: mtl } : {} };
1877
2793
  }, [mtl, parseOptions]);
1878
2794
  const fetched = usePolyMesh(src ?? "", mergedOptions);
1879
- const sourcePolygons = src ? fetched.polygons : polygonsProp ?? [];
2795
+ const externalPolygons = src ? fetched.polygons : polygonsProp ?? [];
2796
+ const [localPolygons, setLocalPolygons] = useState3(null);
2797
+ const prevExternalRef = useRef5(externalPolygons);
2798
+ if (prevExternalRef.current !== externalPolygons) {
2799
+ prevExternalRef.current = externalPolygons;
2800
+ if (localPolygons !== null) setLocalPolygons(null);
2801
+ }
2802
+ const sourcePolygons = localPolygons ?? externalPolygons;
2803
+ const hasRenderProp = typeof children === "function";
2804
+ const renderPolygon = hasRenderProp ? children : null;
2805
+ const staticChildren = hasRenderProp ? null : children;
1880
2806
  const polygons = useMemo7(
1881
2807
  () => autoCenter ? recenterPolygons(sourcePolygons) : sourcePolygons,
1882
2808
  [sourcePolygons, autoCenter]
1883
2809
  );
1884
2810
  const transform = buildTransform(position, scale, rotation);
2811
+ const transformOrigin = useMemo7(() => {
2812
+ if (polygons.length === 0) return void 0;
2813
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
2814
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
2815
+ for (const poly of polygons) {
2816
+ for (const v of poly.vertices) {
2817
+ if (v[0] < minX) minX = v[0];
2818
+ if (v[0] > maxX) maxX = v[0];
2819
+ if (v[1] < minY) minY = v[1];
2820
+ if (v[1] > maxY) maxY = v[1];
2821
+ if (v[2] < minZ) minZ = v[2];
2822
+ if (v[2] > maxZ) maxZ = v[2];
2823
+ }
2824
+ }
2825
+ if (!Number.isFinite(minX)) return void 0;
2826
+ const tile = 50;
2827
+ const x = (minY + maxY) / 2 * tile;
2828
+ const y = (minX + maxX) / 2 * tile;
2829
+ const z = (minZ + maxZ) / 2 * tile;
2830
+ return `${x}px ${y}px ${z}px`;
2831
+ }, [polygons]);
1885
2832
  const wrapperRef = useRef5(null);
1886
2833
  const propsRef = useRef5({ position, scale, rotation });
1887
2834
  propsRef.current = { position, scale, rotation };
@@ -1897,7 +2844,14 @@ var PolyMesh = forwardRef(function PolyMesh2({
1897
2844
  getRotation: () => propsRef.current.rotation,
1898
2845
  getScale: () => propsRef.current.scale,
1899
2846
  getPolygons: () => polygonsRef.current,
1900
- rebakeAtlas: () => setBakedRotation(propsRef.current.rotation)
2847
+ rebakeAtlas: () => setBakedRotation(propsRef.current.rotation),
2848
+ updatePolygon(target, partial) {
2849
+ const current = polygonsRef.current;
2850
+ const idx = typeof target === "number" ? target : current.indexOf(target);
2851
+ if (idx < 0 || idx >= current.length) return;
2852
+ Object.assign(current[idx], partial);
2853
+ setLocalPolygons([...current]);
2854
+ }
1901
2855
  }), [id]);
1902
2856
  useImperativeHandle(forwardedRef, () => handle, [handle]);
1903
2857
  useEffect5(() => {
@@ -2047,11 +3001,6 @@ var PolyMesh = forwardRef(function PolyMesh2({
2047
3001
  ["--plz"]: (localDir[2] / len).toFixed(4)
2048
3002
  };
2049
3003
  }, [effectiveTextureLighting, rotation, sceneDirectionalLight]);
2050
- const wrapperStyle = {
2051
- transform,
2052
- ...dynamicLightOverride,
2053
- ...style
2054
- };
2055
3004
  const bakedDirectional = useMemo7(() => {
2056
3005
  if (!effectiveDirectional) return effectiveDirectional;
2057
3006
  const rot = bakedRotation ?? [0, 0, 0];
@@ -2062,21 +3011,84 @@ var PolyMesh = forwardRef(function PolyMesh2({
2062
3011
  };
2063
3012
  }, [effectiveDirectional, bakedRotation]);
2064
3013
  const atlasPlans = useMemo7(
2065
- () => !children ? polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
2066
- directionalLight: bakedDirectional,
2067
- ambientLight: effectiveAmbient
2068
- })) : [],
2069
- [children, polygons, bakedDirectional, effectiveAmbient]
3014
+ () => {
3015
+ if (renderPolygon) return [];
3016
+ const repairEdges = buildTextureEdgeRepairSets(polygons);
3017
+ return polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
3018
+ directionalLight: bakedDirectional,
3019
+ ambientLight: effectiveAmbient,
3020
+ textureEdgeRepairEdges: repairEdges[i]
3021
+ }));
3022
+ },
3023
+ [renderPolygon, polygons, bakedDirectional, effectiveAmbient]
2070
3024
  );
2071
3025
  const textureAtlas = useTextureAtlas(
2072
3026
  atlasPlans,
2073
3027
  effectiveTextureLighting,
2074
- atlasScale
3028
+ textureQuality
3029
+ );
3030
+ const solidPaintDefaults = useMemo7(
3031
+ () => !renderPolygon ? getSolidPaintDefaults(atlasPlans, effectiveTextureLighting) : {},
3032
+ [renderPolygon, atlasPlans, effectiveTextureLighting]
2075
3033
  );
2076
- const renderedPolygons = children ? polygons.map((p, i) => (
3034
+ const defaultPaintVars = useMemo7(
3035
+ () => solidPaintVars(solidPaintDefaults),
3036
+ [solidPaintDefaults]
3037
+ );
3038
+ const meshIdRef = useRef5(/* @__PURE__ */ Symbol());
3039
+ const sceneRegisterShadowCaster = sceneCtx?.registerShadowCaster;
3040
+ useEffect5(() => {
3041
+ if (!sceneRegisterShadowCaster) return;
3042
+ if (castShadow && effectiveTextureLighting === "dynamic") {
3043
+ sceneRegisterShadowCaster(meshIdRef.current, polygons);
3044
+ } else {
3045
+ sceneRegisterShadowCaster(meshIdRef.current, null);
3046
+ }
3047
+ return () => {
3048
+ sceneRegisterShadowCaster(meshIdRef.current, null);
3049
+ };
3050
+ }, [sceneRegisterShadowCaster, castShadow, effectiveTextureLighting, polygons]);
3051
+ const shadowLeaves = useMemo7(() => {
3052
+ if (!castShadow || effectiveTextureLighting !== "dynamic" || renderPolygon) return [];
3053
+ const shadowColor = sceneCtx?.shadow?.color ?? "#000000";
3054
+ const shadowOpacity = sceneCtx?.shadow?.opacity ?? 0.25;
3055
+ const parsed = parseHexColor2(shadowColor)?.rgb ?? [0, 0, 0];
3056
+ const shadowColorCss = `rgba(${parsed[0]},${parsed[1]},${parsed[2]},${shadowOpacity})`;
3057
+ const shadowDedupDrop = findOverlappingPolygonDuplicates(polygons, {
3058
+ normalTolerance: 0.1,
3059
+ distanceTolerance: 0.5,
3060
+ overlapFraction: 0.4
3061
+ });
3062
+ const leaves = [];
3063
+ for (const plan of atlasPlans) {
3064
+ if (!plan) continue;
3065
+ if (shadowDedupDrop.has(plan.index)) continue;
3066
+ const borderShape = cssBorderShapeForPlan(plan);
3067
+ leaves.push(
3068
+ /* @__PURE__ */ jsx5(
3069
+ ShadowLeaf,
3070
+ {
3071
+ plan,
3072
+ shadowColorCss,
3073
+ borderShape
3074
+ },
3075
+ `shadow-${plan.index}`
3076
+ )
3077
+ );
3078
+ }
3079
+ return leaves;
3080
+ }, [castShadow, effectiveTextureLighting, renderPolygon, polygons, atlasPlans, sceneCtx?.shadow]);
3081
+ const wrapperStyle = {
3082
+ transform,
3083
+ ...transformOrigin ? { transformOrigin } : null,
3084
+ ...dynamicLightOverride,
3085
+ ...style,
3086
+ ...defaultPaintVars
3087
+ };
3088
+ const renderedPolygons = renderPolygon ? polygons.map((p, i) => (
2077
3089
  // Render-prop: caller controls how each polygon renders. We still
2078
3090
  // wrap in a fragment with key so React reconciliation works.
2079
- /* @__PURE__ */ jsx5(RenderPropPolygon, { polygon: p, index: i, children }, i)
3091
+ /* @__PURE__ */ jsx5(RenderPropPolygon, { polygon: p, index: i, children: renderPolygon }, i)
2080
3092
  )) : textureAtlas.entries.map((entry, index) => {
2081
3093
  if (entry) {
2082
3094
  return /* @__PURE__ */ jsx5(
@@ -2091,7 +3103,22 @@ var PolyMesh = forwardRef(function PolyMesh2({
2091
3103
  }
2092
3104
  const plan = atlasPlans[index];
2093
3105
  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);
3106
+ return isSolidTrianglePlan(plan) ? /* @__PURE__ */ jsx5(
3107
+ TextureTrianglePoly,
3108
+ {
3109
+ entry: plan,
3110
+ textureLighting: effectiveTextureLighting,
3111
+ solidPaintDefaults
3112
+ },
3113
+ plan.index
3114
+ ) : /* @__PURE__ */ jsx5(
3115
+ TextureBorderShapePoly,
3116
+ {
3117
+ entry: plan,
3118
+ solidPaintDefaults
3119
+ },
3120
+ plan.index
3121
+ );
2095
3122
  });
2096
3123
  if (src) {
2097
3124
  if (fetched.loading && fetched.polygons.length === 0) {
@@ -2121,7 +3148,7 @@ var PolyMesh = forwardRef(function PolyMesh2({
2121
3148
  );
2122
3149
  }
2123
3150
  }
2124
- return /* @__PURE__ */ jsx5(
3151
+ return /* @__PURE__ */ jsxs2(
2125
3152
  "div",
2126
3153
  {
2127
3154
  ref: wrapperRef,
@@ -2129,7 +3156,11 @@ var PolyMesh = forwardRef(function PolyMesh2({
2129
3156
  className: `polycss-mesh${className ? ` ${className}` : ""}`,
2130
3157
  style: wrapperStyle,
2131
3158
  ...wrapperHandlers,
2132
- children: renderedPolygons
3159
+ children: [
3160
+ shadowLeaves,
3161
+ renderedPolygons,
3162
+ staticChildren
3163
+ ]
2133
3164
  }
2134
3165
  );
2135
3166
  });
@@ -2138,13 +3169,70 @@ function RenderPropPolygon({
2138
3169
  index,
2139
3170
  children
2140
3171
  }) {
2141
- return /* @__PURE__ */ jsx5(Fragment2, { children: children(polygon, index) });
3172
+ return /* @__PURE__ */ jsx5(Fragment, { children: children(polygon, index) });
3173
+ }
3174
+ function ShadowLeaf({
3175
+ plan,
3176
+ shadowColorCss,
3177
+ borderShape
3178
+ }) {
3179
+ const setRef = useCallback6((el) => {
3180
+ if (!el) return;
3181
+ el.style.setProperty("border-shape", borderShape);
3182
+ }, [borderShape]);
3183
+ return /* @__PURE__ */ jsx5(
3184
+ "q",
3185
+ {
3186
+ ref: setRef,
3187
+ className: "polycss-shadow",
3188
+ style: {
3189
+ transform: `var(--shadow-proj) matrix3d(${plan.matrix})`,
3190
+ color: shadowColorCss,
3191
+ width: plan.canvasW,
3192
+ height: plan.canvasH,
3193
+ ["--pnx"]: plan.normal[0].toFixed(4),
3194
+ ["--pny"]: plan.normal[1].toFixed(4),
3195
+ ["--pnz"]: plan.normal[2].toFixed(4)
3196
+ }
3197
+ }
3198
+ );
3199
+ }
3200
+
3201
+ // src/scene/PolyGround.tsx
3202
+ import { useMemo as useMemo8 } from "react";
3203
+ import { jsx as jsx6 } from "react/jsx-runtime";
3204
+ function PolyGround({
3205
+ size = 6,
3206
+ z = 0,
3207
+ center = [0, 0],
3208
+ color = "#7d848e",
3209
+ className
3210
+ }) {
3211
+ const polygons = useMemo8(() => {
3212
+ const half = size / 2;
3213
+ const [cx, cy] = center;
3214
+ const vertices = [
3215
+ [cx - half, cy - half, z],
3216
+ [cx + half, cy - half, z],
3217
+ [cx + half, cy + half, z],
3218
+ [cx - half, cy + half, z]
3219
+ ];
3220
+ return [{ vertices, color }];
3221
+ }, [size, z, center, color]);
3222
+ return /* @__PURE__ */ jsx6(
3223
+ PolyMesh,
3224
+ {
3225
+ polygons,
3226
+ castShadow: false,
3227
+ className: className ? `polycss-ground ${className}` : "polycss-ground"
3228
+ }
3229
+ );
2142
3230
  }
2143
3231
 
2144
3232
  // src/scene/usePolyMaterial.ts
2145
- import { useMemo as useMemo8 } from "react";
3233
+ import { useMemo as useMemo9 } from "react";
2146
3234
  function usePolyMaterial(options) {
2147
- return useMemo8(
3235
+ return useMemo9(
2148
3236
  () => ({ texture: options.texture, key: options.key }),
2149
3237
  // eslint-disable-next-line react-hooks/exhaustive-deps
2150
3238
  [options.texture, options.key]
@@ -2152,8 +3240,13 @@ function usePolyMaterial(options) {
2152
3240
  }
2153
3241
 
2154
3242
  // src/shapes/Poly.tsx
2155
- import { memo as memo4, useMemo as useMemo9 } from "react";
2156
- import { jsx as jsx6 } from "react/jsx-runtime";
3243
+ import { memo as memo4, useMemo as useMemo10 } from "react";
3244
+ import { jsx as jsx7 } from "react/jsx-runtime";
3245
+ var DIRECT_TEXTURE_CSS_DECIMALS = 4;
3246
+ function formatCssLength2(value, decimals = DIRECT_TEXTURE_CSS_DECIMALS) {
3247
+ const next = value.toFixed(decimals).replace(/\.?0+$/, "");
3248
+ return Number(next) === 0 || Object.is(Number(next), -0) ? "0" : `${next}px`;
3249
+ }
2157
3250
  function isAxisAlignedRectUVs(uvs) {
2158
3251
  if (uvs.length !== 4) return null;
2159
3252
  const us = [...new Set(uvs.map((uv) => uv[0]))].sort((a, b) => a - b);
@@ -2183,18 +3276,16 @@ function MaterialDirectPoly({
2183
3276
  const { u0, u1, v0, v1 } = uvRect;
2184
3277
  const du = u1 - u0;
2185
3278
  const dv = v1 - v0;
2186
- const sourceW = plan.canvasW / du;
2187
- const sourceH = plan.canvasH / dv;
3279
+ const sourceW = 1 / du;
3280
+ const sourceH = 1 / dv;
2188
3281
  const vMax = Math.max(v0, v1);
2189
- const offsetX = u0 * sourceW;
2190
- const offsetY = (1 - vMax) * sourceH;
3282
+ const offsetX = u0 / du;
3283
+ const offsetY = (1 - vMax) / dv;
2191
3284
  const style = {
2192
- width: plan.canvasW,
2193
- height: plan.canvasH,
2194
- transform: `matrix3d(${plan.matrix})`,
3285
+ transform: `matrix3d(${plan.canonicalMatrix})`,
2195
3286
  backgroundImage: `url(${material.texture})`,
2196
- backgroundSize: `${sourceW}px ${sourceH}px`,
2197
- backgroundPosition: `-${offsetX}px -${offsetY}px`,
3287
+ backgroundSize: `${formatCssLength2(sourceW)} ${formatCssLength2(sourceH)}`,
3288
+ backgroundPosition: `${formatCssLength2(-offsetX)} ${formatCssLength2(-offsetY)}`,
2198
3289
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
2199
3290
  ...styleProp
2200
3291
  };
@@ -2202,7 +3293,7 @@ function MaterialDirectPoly({
2202
3293
  Object.entries(plan.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
2203
3294
  ) : {};
2204
3295
  const elementClassName = className?.trim() || void 0;
2205
- return /* @__PURE__ */ jsx6(
3296
+ return /* @__PURE__ */ jsx7(
2206
3297
  "i",
2207
3298
  {
2208
3299
  className: elementClassName,
@@ -2245,17 +3336,17 @@ function PolyInner({
2245
3336
  pointerEvents: pointerEventsProp,
2246
3337
  context,
2247
3338
  textureLighting: textureLightingProp,
2248
- atlasScale: atlasScaleProp,
3339
+ textureQuality: textureQualityProp,
2249
3340
  baseColor: baseColorProp,
2250
3341
  ...dataAttrs
2251
3342
  }) {
2252
3343
  const tileSize = context?.tileSize ?? 50;
2253
3344
  const layerElevation = context?.layerElevation ?? tileSize;
2254
3345
  const textureLighting = textureLightingProp ?? context?.textureLighting ?? "baked";
2255
- const atlasScale = atlasScaleProp ?? context?.atlasScale;
3346
+ const textureQuality = textureQualityProp ?? context?.textureQuality;
2256
3347
  const polygonColor = baseColorProp ?? color;
2257
3348
  const effectiveTexture = material?.texture ?? texture;
2258
- const atlasPlan = useMemo9(
3349
+ const atlasPlan = useMemo10(
2259
3350
  () => computeTextureAtlasPlan(
2260
3351
  { vertices, color: polygonColor, texture: effectiveTexture, uvs, data },
2261
3352
  0,
@@ -2276,15 +3367,15 @@ function PolyInner({
2276
3367
  context?.directionalLight
2277
3368
  ]
2278
3369
  );
2279
- const materialUvRect = useMemo9(
3370
+ const materialUvRect = useMemo10(
2280
3371
  () => material && uvs ? isAxisAlignedRectUVs(uvs) : null,
2281
3372
  [material, uvs]
2282
3373
  );
2283
- const atlasPlans = useMemo9(
3374
+ const atlasPlans = useMemo10(
2284
3375
  () => materialUvRect ? [] : [atlasPlan],
2285
3376
  [materialUvRect, atlasPlan]
2286
3377
  );
2287
- const textureAtlas = useTextureAtlas(atlasPlans, textureLighting, atlasScale);
3378
+ const textureAtlas = useTextureAtlas(atlasPlans, textureLighting, textureQuality);
2288
3379
  const domEventHandlers = {
2289
3380
  onClick,
2290
3381
  onDoubleClick,
@@ -2333,7 +3424,7 @@ function PolyInner({
2333
3424
  const wrapperTransform = transformParts.length > 0 ? transformParts.join(" ") : void 0;
2334
3425
  let front = null;
2335
3426
  if (materialUvRect && material && atlasPlan) {
2336
- front = /* @__PURE__ */ jsx6(
3427
+ front = /* @__PURE__ */ jsx7(
2337
3428
  MaterialDirectPoly,
2338
3429
  {
2339
3430
  plan: atlasPlan,
@@ -2349,7 +3440,7 @@ function PolyInner({
2349
3440
  } else {
2350
3441
  const atlasEntry = textureAtlas.entries[0];
2351
3442
  if (atlasEntry) {
2352
- front = /* @__PURE__ */ jsx6(
3443
+ front = /* @__PURE__ */ jsx7(
2353
3444
  TextureAtlasPoly,
2354
3445
  {
2355
3446
  entry: atlasEntry,
@@ -2363,7 +3454,7 @@ function PolyInner({
2363
3454
  }
2364
3455
  );
2365
3456
  } else if (atlasPlan && !atlasPlan.texture) {
2366
- front = isSolidTrianglePlan(atlasPlan) ? /* @__PURE__ */ jsx6(
3457
+ front = isSolidTrianglePlan(atlasPlan) ? /* @__PURE__ */ jsx7(
2367
3458
  TextureTrianglePoly,
2368
3459
  {
2369
3460
  entry: atlasPlan,
@@ -2374,7 +3465,18 @@ function PolyInner({
2374
3465
  domEventHandlers,
2375
3466
  pointerEvents: pointerEventsProp ?? "auto"
2376
3467
  }
2377
- ) : /* @__PURE__ */ jsx6(
3468
+ ) : isProjectiveQuadPlan(atlasPlan) ? /* @__PURE__ */ jsx7(
3469
+ TextureProjectiveSolidPoly,
3470
+ {
3471
+ entry: atlasPlan,
3472
+ textureLighting,
3473
+ className,
3474
+ style: styleProp,
3475
+ domAttrs,
3476
+ domEventHandlers,
3477
+ pointerEvents: pointerEventsProp ?? "auto"
3478
+ }
3479
+ ) : /* @__PURE__ */ jsx7(
2378
3480
  TextureBorderShapePoly,
2379
3481
  {
2380
3482
  entry: atlasPlan,
@@ -2389,14 +3491,14 @@ function PolyInner({
2389
3491
  }
2390
3492
  if (!front) return null;
2391
3493
  if (!wrapperTransform) return front;
2392
- return /* @__PURE__ */ jsx6(
3494
+ return /* @__PURE__ */ jsx7(
2393
3495
  "div",
2394
3496
  {
2395
3497
  className: "polycss-poly-wrapper",
2396
3498
  style: {
3499
+ transform: wrapperTransform,
2397
3500
  position: "absolute",
2398
- transformStyle: "preserve-3d",
2399
- transform: wrapperTransform
3501
+ transformStyle: "preserve-3d"
2400
3502
  },
2401
3503
  children: front
2402
3504
  }
@@ -2404,11 +3506,426 @@ function PolyInner({
2404
3506
  }
2405
3507
  var Poly = memo4(PolyInner);
2406
3508
 
2407
- // src/controls/PolyOrbitControls.tsx
2408
- import { useEffect as useEffect6, useRef as useRef6 } from "react";
3509
+ // src/controls/PolyFirstPersonControls.tsx
3510
+ import { useEffect as useEffect6, useRef as useRef6, useImperativeHandle as useImperativeHandle2, forwardRef as forwardRef2 } from "react";
3511
+ import { BASE_TILE as BASE_TILE3 } from "@layoutit/polycss-core";
3512
+ var DEFAULTS = {
3513
+ enabled: true,
3514
+ lookEnabled: true,
3515
+ moveEnabled: true,
3516
+ jumpEnabled: true,
3517
+ crouchEnabled: true,
3518
+ lookSensitivity: 0.15,
3519
+ invertY: false,
3520
+ moveSpeed: 5,
3521
+ jumpVelocity: 7,
3522
+ gravity: 18,
3523
+ eyeHeight: 1.7,
3524
+ crouchHeight: 1,
3525
+ groundZ: 0,
3526
+ minPitch: 5,
3527
+ maxPitch: 175
3528
+ };
3529
+ function resolveOptions(base, partial) {
3530
+ return {
3531
+ enabled: partial.enabled ?? base.enabled,
3532
+ lookEnabled: partial.lookEnabled ?? base.lookEnabled,
3533
+ moveEnabled: partial.moveEnabled ?? base.moveEnabled,
3534
+ jumpEnabled: partial.jumpEnabled ?? base.jumpEnabled,
3535
+ crouchEnabled: partial.crouchEnabled ?? base.crouchEnabled,
3536
+ lookSensitivity: partial.lookSensitivity ?? base.lookSensitivity,
3537
+ invertY: partial.invertY ?? base.invertY,
3538
+ moveSpeed: partial.moveSpeed ?? base.moveSpeed,
3539
+ jumpVelocity: partial.jumpVelocity ?? base.jumpVelocity,
3540
+ gravity: partial.gravity ?? base.gravity,
3541
+ eyeHeight: partial.eyeHeight ?? base.eyeHeight,
3542
+ crouchHeight: partial.crouchHeight ?? base.crouchHeight,
3543
+ groundZ: partial.groundZ ?? base.groundZ,
3544
+ minPitch: partial.minPitch ?? base.minPitch,
3545
+ maxPitch: partial.maxPitch ?? base.maxPitch
3546
+ };
3547
+ }
3548
+ var FORWARD_KEYS = /* @__PURE__ */ new Set(["KeyW", "ArrowUp"]);
3549
+ var BACK_KEYS = /* @__PURE__ */ new Set(["KeyS", "ArrowDown"]);
3550
+ var LEFT_KEYS = /* @__PURE__ */ new Set(["KeyA", "ArrowLeft"]);
3551
+ var RIGHT_KEYS = /* @__PURE__ */ new Set(["KeyD", "ArrowRight"]);
3552
+ var JUMP_KEYS = /* @__PURE__ */ new Set(["Space"]);
3553
+ var CROUCH_KEYS = /* @__PURE__ */ new Set(["ControlLeft", "ControlRight"]);
3554
+ function isFpvKey(code) {
3555
+ return FORWARD_KEYS.has(code) || BACK_KEYS.has(code) || LEFT_KEYS.has(code) || RIGHT_KEYS.has(code) || JUMP_KEYS.has(code) || CROUCH_KEYS.has(code);
3556
+ }
3557
+ function makeRegistry() {
3558
+ return { change: [], start: [], end: [] };
3559
+ }
3560
+ function emitEvent(registry, type) {
3561
+ const list = [...registry[type]];
3562
+ for (const fn of list) {
3563
+ try {
3564
+ fn();
3565
+ } catch {
3566
+ }
3567
+ }
3568
+ }
3569
+ var PolyFirstPersonControls = forwardRef2(function PolyFirstPersonControls2(props, ref) {
3570
+ const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
3571
+ const onChangeRef = useRef6(props.onChange);
3572
+ const onInteractionStartRef = useRef6(props.onInteractionStart);
3573
+ const onInteractionEndRef = useRef6(props.onInteractionEnd);
3574
+ useEffect6(() => {
3575
+ onChangeRef.current = props.onChange;
3576
+ onInteractionStartRef.current = props.onInteractionStart;
3577
+ onInteractionEndRef.current = props.onInteractionEnd;
3578
+ });
3579
+ const optsRef = useRef6(resolveOptions(DEFAULTS, props));
3580
+ const cameraOriginRef = useRef6([0, 0, 0]);
3581
+ const rafIdRef = useRef6(null);
3582
+ const lastTimeRef = useRef6(0);
3583
+ const stoppedRef = useRef6(false);
3584
+ const pointerLockedRef = useRef6(false);
3585
+ const interactingRef = useRef6(false);
3586
+ const keysHeldRef = useRef6(/* @__PURE__ */ new Set());
3587
+ const verticalVelRef = useRef6(0);
3588
+ const jumpOffsetRef = useRef6(0);
3589
+ const registryRef = useRef6(makeRegistry());
3590
+ useImperativeHandle2(ref, () => ({
3591
+ update(partial) {
3592
+ const prev = optsRef.current;
3593
+ optsRef.current = resolveOptions(prev, partial);
3594
+ if (!stoppedRef.current) {
3595
+ const host = cameraElRef.current;
3596
+ if (host) host.style.cursor = optsRef.current.lookEnabled ? "crosshair" : "";
3597
+ }
3598
+ },
3599
+ resume() {
3600
+ if (!stoppedRef.current) return;
3601
+ stoppedRef.current = false;
3602
+ const host = cameraElRef.current;
3603
+ if (host) host.style.cursor = optsRef.current.lookEnabled ? "crosshair" : "";
3604
+ startLoop();
3605
+ },
3606
+ pause() {
3607
+ if (stoppedRef.current) return;
3608
+ stoppedRef.current = true;
3609
+ stopLoop();
3610
+ const host = cameraElRef.current;
3611
+ if (host) host.style.cursor = "";
3612
+ if (interactingRef.current) {
3613
+ interactingRef.current = false;
3614
+ emitEvent(registryRef.current, "end");
3615
+ try {
3616
+ onInteractionEndRef.current?.();
3617
+ } catch {
3618
+ }
3619
+ }
3620
+ },
3621
+ destroy() {
3622
+ stoppedRef.current = true;
3623
+ stopLoop();
3624
+ },
3625
+ lock() {
3626
+ const opts = optsRef.current;
3627
+ if (!opts.enabled || !opts.lookEnabled || stoppedRef.current) return;
3628
+ const host = cameraElRef.current;
3629
+ try {
3630
+ host?.requestPointerLock();
3631
+ } catch {
3632
+ }
3633
+ },
3634
+ unlock() {
3635
+ if (pointerLockedRef.current) {
3636
+ const host = cameraElRef.current;
3637
+ try {
3638
+ host?.ownerDocument?.exitPointerLock();
3639
+ } catch {
3640
+ }
3641
+ }
3642
+ },
3643
+ isLocked() {
3644
+ return pointerLockedRef.current;
3645
+ },
3646
+ getOrigin() {
3647
+ const o = cameraOriginRef.current;
3648
+ return [o[0], o[1], o[2]];
3649
+ },
3650
+ setOrigin(origin) {
3651
+ cameraOriginRef.current[0] = origin[0];
3652
+ cameraOriginRef.current[1] = origin[1];
3653
+ cameraOriginRef.current[2] = origin[2];
3654
+ syncTargetFromOrigin();
3655
+ emitEvent(registryRef.current, "change");
3656
+ try {
3657
+ onChangeRef.current?.();
3658
+ } catch {
3659
+ }
3660
+ },
3661
+ addEventListener(type, listener) {
3662
+ const arr = registryRef.current[type];
3663
+ if (!arr.includes(listener)) arr.push(listener);
3664
+ },
3665
+ removeEventListener(type, listener) {
3666
+ const arr = registryRef.current[type];
3667
+ const idx = arr.indexOf(listener);
3668
+ if (idx >= 0) arr.splice(idx, 1);
3669
+ },
3670
+ hasEventListener(type, listener) {
3671
+ return registryRef.current[type].includes(listener);
3672
+ }
3673
+ }));
3674
+ function forwardDir(rotX, rotY) {
3675
+ const rx = rotX * Math.PI / 180;
3676
+ const ry = rotY * Math.PI / 180;
3677
+ return [
3678
+ -Math.sin(rx) * Math.cos(ry),
3679
+ -Math.sin(rx) * Math.sin(ry),
3680
+ -Math.cos(rx)
3681
+ ];
3682
+ }
3683
+ function lookOffset() {
3684
+ const host = cameraElRef.current;
3685
+ const perspStr = host ? getComputedStyle(host).perspective : "";
3686
+ const n = parseFloat(perspStr);
3687
+ return (Number.isFinite(n) && n > 0 ? n : 8e3) / BASE_TILE3;
3688
+ }
3689
+ function deriveTarget() {
3690
+ const s = cameraRef.current.state;
3691
+ const f = forwardDir(s.rotX ?? 90, s.rotY ?? 0);
3692
+ const d = lookOffset();
3693
+ const o = cameraOriginRef.current;
3694
+ return [o[0] + f[0] * d, o[1] + f[1] * d, o[2] + f[2] * d];
3695
+ }
3696
+ function syncTargetFromOrigin() {
3697
+ const t = deriveTarget();
3698
+ const handle = cameraRef.current;
3699
+ handle.update({ target: t });
3700
+ applyTransformDirect();
3701
+ store.updateCameraFromRef(handle);
3702
+ }
3703
+ const ANIM_DT_CLAMP = 0.05;
3704
+ function tick(now) {
3705
+ if (rafIdRef.current === null || stoppedRef.current) return;
3706
+ const dt = Math.min(ANIM_DT_CLAMP, lastTimeRef.current ? (now - lastTimeRef.current) / 1e3 : 0.0167);
3707
+ lastTimeRef.current = now;
3708
+ const opts = optsRef.current;
3709
+ if (opts.enabled) {
3710
+ let dirty = false;
3711
+ const s = cameraRef.current.state;
3712
+ const o = cameraOriginRef.current;
3713
+ if (opts.moveEnabled) {
3714
+ let mf = 0, mr = 0;
3715
+ for (const code of keysHeldRef.current) {
3716
+ if (FORWARD_KEYS.has(code)) mf += 1;
3717
+ else if (BACK_KEYS.has(code)) mf -= 1;
3718
+ else if (RIGHT_KEYS.has(code)) mr += 1;
3719
+ else if (LEFT_KEYS.has(code)) mr -= 1;
3720
+ }
3721
+ if (mf !== 0 || mr !== 0) {
3722
+ const rotY = s.rotY ?? 0;
3723
+ const r = rotY * Math.PI / 180;
3724
+ const fx = -Math.cos(r), fy = -Math.sin(r);
3725
+ const rx = -Math.sin(r), ry = Math.cos(r);
3726
+ const len = Math.hypot(mf, mr) || 1;
3727
+ const step = opts.moveSpeed * dt;
3728
+ o[0] += (fx * mf + rx * mr) / len * step;
3729
+ o[1] += (fy * mf + ry * mr) / len * step;
3730
+ dirty = true;
3731
+ }
3732
+ }
3733
+ const crouched = opts.crouchEnabled && (keysHeldRef.current.has("ControlLeft") || keysHeldRef.current.has("ControlRight"));
3734
+ const baseHeight = crouched ? opts.crouchHeight : opts.eyeHeight;
3735
+ if (opts.jumpEnabled && (verticalVelRef.current !== 0 || jumpOffsetRef.current > 0)) {
3736
+ verticalVelRef.current -= opts.gravity * dt;
3737
+ jumpOffsetRef.current += verticalVelRef.current * dt;
3738
+ if (jumpOffsetRef.current <= 0) {
3739
+ jumpOffsetRef.current = 0;
3740
+ verticalVelRef.current = 0;
3741
+ }
3742
+ } else if (!opts.jumpEnabled) {
3743
+ jumpOffsetRef.current = 0;
3744
+ verticalVelRef.current = 0;
3745
+ }
3746
+ const originZ = opts.groundZ + baseHeight + jumpOffsetRef.current;
3747
+ if (Math.abs(o[2] - originZ) > 1e-4) {
3748
+ o[2] = originZ;
3749
+ dirty = true;
3750
+ }
3751
+ if (dirty) {
3752
+ const t = deriveTarget();
3753
+ const handle = cameraRef.current;
3754
+ handle.update({ target: t });
3755
+ applyTransformDirect();
3756
+ store.updateCameraFromRef(handle);
3757
+ emitEvent(registryRef.current, "change");
3758
+ try {
3759
+ onChangeRef.current?.();
3760
+ } catch {
3761
+ }
3762
+ }
3763
+ }
3764
+ rafIdRef.current = requestAnimationFrame(tick);
3765
+ }
3766
+ function startLoop() {
3767
+ if (rafIdRef.current !== null || stoppedRef.current) return;
3768
+ lastTimeRef.current = 0;
3769
+ rafIdRef.current = requestAnimationFrame(tick);
3770
+ }
3771
+ function stopLoop() {
3772
+ if (rafIdRef.current === null) return;
3773
+ cancelAnimationFrame(rafIdRef.current);
3774
+ rafIdRef.current = null;
3775
+ }
3776
+ useEffect6(() => {
3777
+ const host = cameraElRef.current;
3778
+ if (!host) return;
3779
+ const doc = host.ownerDocument ?? document;
3780
+ const win = doc.defaultView ?? globalThis;
3781
+ stoppedRef.current = false;
3782
+ pointerLockedRef.current = false;
3783
+ interactingRef.current = false;
3784
+ keysHeldRef.current.clear();
3785
+ verticalVelRef.current = 0;
3786
+ jumpOffsetRef.current = 0;
3787
+ const s = cameraRef.current.state;
3788
+ const t = s.target ?? [0, 0, 0];
3789
+ const opts = optsRef.current;
3790
+ cameraOriginRef.current = [t[0], t[1], opts.groundZ + opts.eyeHeight];
3791
+ syncTargetFromOrigin();
3792
+ host.style.cursor = opts.lookEnabled ? "crosshair" : "";
3793
+ const computedPersp = win.getComputedStyle(host).perspective;
3794
+ const persp = parseFloat(computedPersp);
3795
+ const effectivePersp = Number.isFinite(persp) && persp > 0 ? persp : 2e3;
3796
+ host.style.setProperty("--polycss-fpv-perspective", `${effectivePersp}px`);
3797
+ host.classList.add("polycss-fpv-host");
3798
+ const onHostClick = () => {
3799
+ const o = optsRef.current;
3800
+ if (!o.enabled || !o.lookEnabled || stoppedRef.current || pointerLockedRef.current) return;
3801
+ try {
3802
+ host.requestPointerLock();
3803
+ } catch {
3804
+ }
3805
+ };
3806
+ const onPointerLockChange = () => {
3807
+ const locked = doc.pointerLockElement === host;
3808
+ if (locked === pointerLockedRef.current) return;
3809
+ pointerLockedRef.current = locked;
3810
+ if (locked) {
3811
+ interactingRef.current = true;
3812
+ emitEvent(registryRef.current, "start");
3813
+ try {
3814
+ onInteractionStartRef.current?.();
3815
+ } catch {
3816
+ }
3817
+ } else {
3818
+ if (interactingRef.current) {
3819
+ interactingRef.current = false;
3820
+ emitEvent(registryRef.current, "end");
3821
+ try {
3822
+ onInteractionEndRef.current?.();
3823
+ } catch {
3824
+ }
3825
+ }
3826
+ }
3827
+ };
3828
+ const onMouseMove = (e) => {
3829
+ if (!pointerLockedRef.current || stoppedRef.current) return;
3830
+ const o = optsRef.current;
3831
+ if (!o.enabled || !o.lookEnabled) return;
3832
+ const dx = e.movementX ?? 0;
3833
+ const dy = e.movementY ?? 0;
3834
+ if (dx === 0 && dy === 0) return;
3835
+ const handle = cameraRef.current;
3836
+ const sceneOpts = handle.state;
3837
+ const sens = o.lookSensitivity;
3838
+ const dyDir = o.invertY ? -1 : 1;
3839
+ const rotY = (((sceneOpts.rotY ?? 0) - dx * sens) % 360 + 360) % 360;
3840
+ let rotX = (sceneOpts.rotX ?? 90) - dy * sens * dyDir;
3841
+ if (rotX < o.minPitch) rotX = o.minPitch;
3842
+ else if (rotX > o.maxPitch) rotX = o.maxPitch;
3843
+ const f = forwardDir(rotX, rotY);
3844
+ const d = lookOffset();
3845
+ const origin = cameraOriginRef.current;
3846
+ const target = [
3847
+ origin[0] + f[0] * d,
3848
+ origin[1] + f[1] * d,
3849
+ origin[2] + f[2] * d
3850
+ ];
3851
+ handle.update({ rotX, rotY, target });
3852
+ applyTransformDirect();
3853
+ store.updateCameraFromRef(handle);
3854
+ emitEvent(registryRef.current, "change");
3855
+ try {
3856
+ onChangeRef.current?.();
3857
+ } catch {
3858
+ }
3859
+ };
3860
+ const onKeyDown = (e) => {
3861
+ const o = optsRef.current;
3862
+ if (!o.enabled || stoppedRef.current) return;
3863
+ if (!isFpvKey(e.code)) return;
3864
+ if (!pointerLockedRef.current && !o.moveEnabled) return;
3865
+ if (JUMP_KEYS.has(e.code)) {
3866
+ if (!o.jumpEnabled) return;
3867
+ e.preventDefault();
3868
+ if (!keysHeldRef.current.has(e.code) && verticalVelRef.current === 0 && jumpOffsetRef.current === 0) {
3869
+ verticalVelRef.current = o.jumpVelocity;
3870
+ }
3871
+ keysHeldRef.current.add(e.code);
3872
+ return;
3873
+ }
3874
+ if (CROUCH_KEYS.has(e.code) && !o.crouchEnabled) return;
3875
+ if (!o.moveEnabled && !CROUCH_KEYS.has(e.code)) return;
3876
+ e.preventDefault();
3877
+ keysHeldRef.current.add(e.code);
3878
+ };
3879
+ const onKeyUp = (e) => {
3880
+ if (!isFpvKey(e.code)) return;
3881
+ keysHeldRef.current.delete(e.code);
3882
+ };
3883
+ const onBlur = () => {
3884
+ keysHeldRef.current.clear();
3885
+ };
3886
+ host.addEventListener("click", onHostClick);
3887
+ doc.addEventListener("pointerlockchange", onPointerLockChange);
3888
+ doc.addEventListener("mousemove", onMouseMove);
3889
+ win.addEventListener("keydown", onKeyDown);
3890
+ win.addEventListener("keyup", onKeyUp);
3891
+ win.addEventListener("blur", onBlur);
3892
+ startLoop();
3893
+ return () => {
3894
+ stoppedRef.current = true;
3895
+ stopLoop();
3896
+ host.removeEventListener("click", onHostClick);
3897
+ doc.removeEventListener("pointerlockchange", onPointerLockChange);
3898
+ doc.removeEventListener("mousemove", onMouseMove);
3899
+ win.removeEventListener("keydown", onKeyDown);
3900
+ win.removeEventListener("keyup", onKeyUp);
3901
+ win.removeEventListener("blur", onBlur);
3902
+ host.style.cursor = "";
3903
+ host.classList.remove("polycss-fpv-host");
3904
+ host.style.removeProperty("--polycss-fpv-perspective");
3905
+ keysHeldRef.current.clear();
3906
+ if (pointerLockedRef.current) {
3907
+ try {
3908
+ doc.exitPointerLock();
3909
+ } catch {
3910
+ }
3911
+ }
3912
+ };
3913
+ }, [cameraElRef, cameraRef, applyTransformDirect, store]);
3914
+ useEffect6(() => {
3915
+ optsRef.current = resolveOptions(optsRef.current, props);
3916
+ const host = cameraElRef.current;
3917
+ if (host && !stoppedRef.current) {
3918
+ host.style.cursor = optsRef.current.lookEnabled ? "crosshair" : "";
3919
+ }
3920
+ });
3921
+ return null;
3922
+ });
3923
+
3924
+ // src/controls/PolyMapControls.tsx
3925
+ import { useEffect as useEffect7, useRef as useRef7 } from "react";
2409
3926
 
2410
3927
  // src/controls/sharedControls.ts
2411
- import { BASE_TILE as BASE_TILE2 } from "@layoutit/polycss-core";
3928
+ import { BASE_TILE as BASE_TILE4 } from "@layoutit/polycss-core";
2412
3929
  var POINTER_DRAG_SPEED = 4;
2413
3930
  function invertFactor(invert) {
2414
3931
  if (invert === true) return -1;
@@ -2429,7 +3946,7 @@ function applyPan(dx, dy, s, handle, _invert) {
2429
3946
  const cosRotX = cosRotXRaw >= 0 ? Math.max(0.1, cosRotXRaw) : Math.min(-0.1, cosRotXRaw);
2430
3947
  const cZ = Math.cos(s.rotY * Math.PI / 180);
2431
3948
  const sZ = Math.sin(s.rotY * Math.PI / 180);
2432
- const k = z * BASE_TILE2;
3949
+ const k = z * BASE_TILE4;
2433
3950
  const targetD0 = (dx * sZ - dy * cZ / cosRotX) / k;
2434
3951
  const targetD1 = -(dx * cZ + dy * sZ / cosRotX) / k;
2435
3952
  const t = s.target;
@@ -2562,8 +4079,8 @@ function makeAnimateEffect({
2562
4079
  };
2563
4080
  }
2564
4081
 
2565
- // src/controls/PolyOrbitControls.tsx
2566
- function PolyOrbitControls({
4082
+ // src/controls/PolyMapControls.tsx
4083
+ function PolyMapControls({
2567
4084
  drag = true,
2568
4085
  wheel = true,
2569
4086
  dolly = false,
@@ -2578,19 +4095,19 @@ function PolyOrbitControls({
2578
4095
  onInteractionEnd
2579
4096
  }) {
2580
4097
  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(() => {
4098
+ const dragRef = useRef7(drag);
4099
+ const wheelRef = useRef7(wheel);
4100
+ const dollyRef = useRef7(dolly);
4101
+ const invertRef = useRef7(invert);
4102
+ const zoomMinRef = useRef7(minZoom);
4103
+ const zoomMaxRef = useRef7(maxZoom);
4104
+ const distanceMinRef = useRef7(minDistance);
4105
+ const distanceMaxRef = useRef7(maxDistance);
4106
+ const animateRef = useRef7(animate);
4107
+ const onChangeRef = useRef7(onChange);
4108
+ const onInteractionStartRef = useRef7(onInteractionStart);
4109
+ const onInteractionEndRef = useRef7(onInteractionEnd);
4110
+ useEffect7(() => {
2594
4111
  dragRef.current = drag;
2595
4112
  wheelRef.current = wheel;
2596
4113
  dollyRef.current = dolly;
@@ -2614,7 +4131,7 @@ function PolyOrbitControls({
2614
4131
  try {
2615
4132
  fn(cameraSnapshot());
2616
4133
  } catch (err) {
2617
- console.error("[polycss/react] PolyOrbitControls onChange threw:", err);
4134
+ console.error("[polycss/react] PolyMapControls onChange threw:", err);
2618
4135
  }
2619
4136
  };
2620
4137
  const fireStart = () => {
@@ -2623,7 +4140,7 @@ function PolyOrbitControls({
2623
4140
  try {
2624
4141
  fn(cameraSnapshot());
2625
4142
  } catch (err) {
2626
- console.error("[polycss/react] PolyOrbitControls onInteractionStart threw:", err);
4143
+ console.error("[polycss/react] PolyMapControls onInteractionStart threw:", err);
2627
4144
  }
2628
4145
  };
2629
4146
  const fireEnd = () => {
@@ -2632,11 +4149,11 @@ function PolyOrbitControls({
2632
4149
  try {
2633
4150
  fn(cameraSnapshot());
2634
4151
  } catch (err) {
2635
- console.error("[polycss/react] PolyOrbitControls onInteractionEnd threw:", err);
4152
+ console.error("[polycss/react] PolyMapControls onInteractionEnd threw:", err);
2636
4153
  }
2637
4154
  };
2638
- const animationPausedShared = useRef6({ value: false }).current;
2639
- useEffect6(() => {
4155
+ const animationPausedShared = useRef7({ value: false }).current;
4156
+ useEffect7(() => {
2640
4157
  if (!drag) return;
2641
4158
  const el = cameraElRef.current;
2642
4159
  if (!el) return;
@@ -2673,9 +4190,9 @@ function PolyOrbitControls({
2673
4190
  pointer = { x: e.clientX, y: e.clientY };
2674
4191
  const handle = cameraRef.current;
2675
4192
  if (e.shiftKey) {
2676
- buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2677
- } else {
2678
4193
  buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
4194
+ } else {
4195
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2679
4196
  }
2680
4197
  applyTransformDirect();
2681
4198
  store.updateCameraFromRef(handle);
@@ -2709,7 +4226,7 @@ function PolyOrbitControls({
2709
4226
  const dy = e.clientY - rightPointer.y;
2710
4227
  rightPointer = { x: e.clientX, y: e.clientY };
2711
4228
  const handle = cameraRef.current;
2712
- buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
4229
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2713
4230
  applyTransformDirect();
2714
4231
  store.updateCameraFromRef(handle);
2715
4232
  fireChange();
@@ -2746,12 +4263,12 @@ function PolyOrbitControls({
2746
4263
  el.style.userSelect = "";
2747
4264
  };
2748
4265
  }, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
2749
- useEffect6(
4266
+ useEffect7(
2750
4267
  () => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
2751
4268
  // eslint-disable-next-line react-hooks/exhaustive-deps
2752
4269
  [wheel, applyTransformDirect, cameraElRef, cameraRef, store]
2753
4270
  );
2754
- useEffect6(
4271
+ useEffect7(
2755
4272
  () => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
2756
4273
  // eslint-disable-next-line react-hooks/exhaustive-deps
2757
4274
  [!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
@@ -2759,9 +4276,9 @@ function PolyOrbitControls({
2759
4276
  return null;
2760
4277
  }
2761
4278
 
2762
- // src/controls/PolyMapControls.tsx
2763
- import { useEffect as useEffect7, useRef as useRef7 } from "react";
2764
- function PolyMapControls({
4279
+ // src/controls/PolyOrbitControls.tsx
4280
+ import { useEffect as useEffect8, useRef as useRef8 } from "react";
4281
+ function PolyOrbitControls({
2765
4282
  drag = true,
2766
4283
  wheel = true,
2767
4284
  dolly = false,
@@ -2776,19 +4293,19 @@ function PolyMapControls({
2776
4293
  onInteractionEnd
2777
4294
  }) {
2778
4295
  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(() => {
4296
+ const dragRef = useRef8(drag);
4297
+ const wheelRef = useRef8(wheel);
4298
+ const dollyRef = useRef8(dolly);
4299
+ const invertRef = useRef8(invert);
4300
+ const zoomMinRef = useRef8(minZoom);
4301
+ const zoomMaxRef = useRef8(maxZoom);
4302
+ const distanceMinRef = useRef8(minDistance);
4303
+ const distanceMaxRef = useRef8(maxDistance);
4304
+ const animateRef = useRef8(animate);
4305
+ const onChangeRef = useRef8(onChange);
4306
+ const onInteractionStartRef = useRef8(onInteractionStart);
4307
+ const onInteractionEndRef = useRef8(onInteractionEnd);
4308
+ useEffect8(() => {
2792
4309
  dragRef.current = drag;
2793
4310
  wheelRef.current = wheel;
2794
4311
  dollyRef.current = dolly;
@@ -2812,7 +4329,7 @@ function PolyMapControls({
2812
4329
  try {
2813
4330
  fn(cameraSnapshot());
2814
4331
  } catch (err) {
2815
- console.error("[polycss/react] PolyMapControls onChange threw:", err);
4332
+ console.error("[polycss/react] PolyOrbitControls onChange threw:", err);
2816
4333
  }
2817
4334
  };
2818
4335
  const fireStart = () => {
@@ -2821,7 +4338,7 @@ function PolyMapControls({
2821
4338
  try {
2822
4339
  fn(cameraSnapshot());
2823
4340
  } catch (err) {
2824
- console.error("[polycss/react] PolyMapControls onInteractionStart threw:", err);
4341
+ console.error("[polycss/react] PolyOrbitControls onInteractionStart threw:", err);
2825
4342
  }
2826
4343
  };
2827
4344
  const fireEnd = () => {
@@ -2830,11 +4347,11 @@ function PolyMapControls({
2830
4347
  try {
2831
4348
  fn(cameraSnapshot());
2832
4349
  } catch (err) {
2833
- console.error("[polycss/react] PolyMapControls onInteractionEnd threw:", err);
4350
+ console.error("[polycss/react] PolyOrbitControls onInteractionEnd threw:", err);
2834
4351
  }
2835
4352
  };
2836
- const animationPausedShared = useRef7({ value: false }).current;
2837
- useEffect7(() => {
4353
+ const animationPausedShared = useRef8({ value: false }).current;
4354
+ useEffect8(() => {
2838
4355
  if (!drag) return;
2839
4356
  const el = cameraElRef.current;
2840
4357
  if (!el) return;
@@ -2871,9 +4388,9 @@ function PolyMapControls({
2871
4388
  pointer = { x: e.clientX, y: e.clientY };
2872
4389
  const handle = cameraRef.current;
2873
4390
  if (e.shiftKey) {
2874
- buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2875
- } else {
2876
4391
  buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
4392
+ } else {
4393
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2877
4394
  }
2878
4395
  applyTransformDirect();
2879
4396
  store.updateCameraFromRef(handle);
@@ -2907,7 +4424,7 @@ function PolyMapControls({
2907
4424
  const dy = e.clientY - rightPointer.y;
2908
4425
  rightPointer = { x: e.clientX, y: e.clientY };
2909
4426
  const handle = cameraRef.current;
2910
- buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
4427
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2911
4428
  applyTransformDirect();
2912
4429
  store.updateCameraFromRef(handle);
2913
4430
  fireChange();
@@ -2944,12 +4461,12 @@ function PolyMapControls({
2944
4461
  el.style.userSelect = "";
2945
4462
  };
2946
4463
  }, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
2947
- useEffect7(
4464
+ useEffect8(
2948
4465
  () => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
2949
4466
  // eslint-disable-next-line react-hooks/exhaustive-deps
2950
4467
  [wheel, applyTransformDirect, cameraElRef, cameraRef, store]
2951
4468
  );
2952
- useEffect7(
4469
+ useEffect8(
2953
4470
  () => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
2954
4471
  // eslint-disable-next-line react-hooks/exhaustive-deps
2955
4472
  [!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
@@ -2961,13 +4478,23 @@ function PolyMapControls({
2961
4478
  import {
2962
4479
  useCallback as useCallback7,
2963
4480
  useContext as useContext4,
2964
- useEffect as useEffect8,
2965
- useMemo as useMemo10,
2966
- useRef as useRef8,
4481
+ useEffect as useEffect9,
4482
+ useMemo as useMemo11,
4483
+ useRef as useRef9,
2967
4484
  useState as useState4
2968
4485
  } from "react";
2969
- import { arrowPolygons, ringPolygons } from "@layoutit/polycss-core";
2970
- import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
4486
+ import {
4487
+ arrowPolygons,
4488
+ DEFAULT_CAMERA_STATE,
4489
+ eulerXYZFromQuat,
4490
+ planePolygons,
4491
+ quatFromAxisAngle,
4492
+ quatFromEulerXYZ,
4493
+ quatMultiply,
4494
+ ringQuadPolygons
4495
+ } from "@layoutit/polycss-core";
4496
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
4497
+ var FALLBACK_CAMERA_STORE = createSceneStore(DEFAULT_CAMERA_STATE);
2971
4498
  var COLOR_X = "#ff3653";
2972
4499
  var COLOR_Y = "#8adb00";
2973
4500
  var COLOR_Z = "#2c8fff";
@@ -2989,8 +4516,10 @@ var SHAFT_HALF_THICKNESS_RATIO = 0.0125;
2989
4516
  var HEAD_LENGTH_RATIO = 0.15;
2990
4517
  var HEAD_HALF_THICKNESS_RATIO = 0.04;
2991
4518
  var RING_RADIUS_RATIO = 1;
2992
- var RING_HALF_THICKNESS_RATIO = 0.012;
2993
- var RING_SEGMENTS = 64;
4519
+ var RING_HALF_THICKNESS_RATIO = 0.02;
4520
+ var RING_QUAD_OUTER_RATIO = 1.04;
4521
+ var PLANE_HALF_SIZE_RATIO = 0.1;
4522
+ var PLANE_OFFSET_RATIO = 0.25;
2994
4523
  var SCREEN_AXIS_DEAD_ZONE_SQ = 1e-4;
2995
4524
  function gizmoLengthForMesh(polygons) {
2996
4525
  if (polygons.length === 0) return FALLBACK_SHAFT_LENGTH;
@@ -3058,6 +4587,23 @@ function userAxisLetterOf(key) {
3058
4587
  const last = key.replace("-", "")[0];
3059
4588
  return last;
3060
4589
  }
4590
+ function isAxisBackFacing(cssAxis, sign, rotXDeg, rotYDeg) {
4591
+ const rx = rotXDeg * Math.PI / 180;
4592
+ const ry = rotYDeg * Math.PI / 180;
4593
+ const a = [0, 0, 0];
4594
+ a[cssAxis] = sign;
4595
+ const bx = a[0] * Math.cos(ry) - a[1] * Math.sin(ry);
4596
+ const by = a[0] * Math.sin(ry) + a[1] * Math.cos(ry);
4597
+ const bz = a[2];
4598
+ const cz = by * Math.sin(rx) + bz * Math.cos(rx);
4599
+ void bx;
4600
+ return cz < 0;
4601
+ }
4602
+ var PLANE_SPECS = [
4603
+ { perpAxis: 2, axisA: 0, axisB: 1, key: "xy", color: COLOR_Z },
4604
+ { perpAxis: 1, axisA: 0, axisB: 2, key: "xz", color: COLOR_Y },
4605
+ { perpAxis: 0, axisA: 1, axisB: 2, key: "yz", color: COLOR_X }
4606
+ ];
3061
4607
  function startAxisDrag(opts) {
3062
4608
  const {
3063
4609
  cssAxis,
@@ -3125,6 +4671,84 @@ function startAxisDrag(opts) {
3125
4671
  window.addEventListener("pointerup", handleUp);
3126
4672
  window.addEventListener("pointercancel", handleUp);
3127
4673
  }
4674
+ function startPlaneDrag(opts) {
4675
+ const {
4676
+ axisA,
4677
+ axisB,
4678
+ probeDistanceCss,
4679
+ wrapper,
4680
+ target,
4681
+ startClientX,
4682
+ startClientY,
4683
+ translationSnap,
4684
+ onChange,
4685
+ onObjectChange,
4686
+ onMouseDown,
4687
+ onMouseUp,
4688
+ onDraggingChanged
4689
+ } = opts;
4690
+ const axisAVec = [0, 0, 0];
4691
+ axisAVec[axisA] = 1;
4692
+ const axisBVec = [0, 0, 0];
4693
+ axisBVec[axisB] = 1;
4694
+ function probe(axisVec) {
4695
+ const el = wrapper.ownerDocument.createElement("div");
4696
+ el.style.position = "absolute";
4697
+ el.style.left = "0";
4698
+ el.style.top = "0";
4699
+ el.style.width = "0";
4700
+ el.style.height = "0";
4701
+ el.style.transform = `translate3d(${axisVec[0] * probeDistanceCss}px, ${axisVec[1] * probeDistanceCss}px, ${axisVec[2] * probeDistanceCss}px)`;
4702
+ wrapper.appendChild(el);
4703
+ const wR = wrapper.getBoundingClientRect();
4704
+ const pR = el.getBoundingClientRect();
4705
+ wrapper.removeChild(el);
4706
+ return {
4707
+ x: (pR.left - wR.left) / probeDistanceCss,
4708
+ y: (pR.top - wR.top) / probeDistanceCss
4709
+ };
4710
+ }
4711
+ const pA = probe(axisAVec);
4712
+ const pB = probe(axisBVec);
4713
+ const det = pA.x * pB.y - pB.x * pA.y;
4714
+ if (Math.abs(det) < SCREEN_AXIS_DEAD_ZONE_SQ) return;
4715
+ const startPos = target.getPosition() ?? [0, 0, 0];
4716
+ onMouseDown?.();
4717
+ onDraggingChanged?.(true);
4718
+ const handleMove = (ev) => {
4719
+ const dx = ev.clientX - startClientX;
4720
+ const dy = ev.clientY - startClientY;
4721
+ let tA = (pB.y * dx - pB.x * dy) / det;
4722
+ let tB = (-pA.y * dx + pA.x * dy) / det;
4723
+ if (translationSnap !== null) {
4724
+ tA = Math.round(tA / translationSnap) * translationSnap;
4725
+ tB = Math.round(tB / translationSnap) * translationSnap;
4726
+ }
4727
+ const next = [
4728
+ startPos[0] + tA * axisAVec[0] + tB * axisBVec[0],
4729
+ startPos[1] + tA * axisAVec[1] + tB * axisBVec[1],
4730
+ startPos[2] + tA * axisAVec[2] + tB * axisBVec[2]
4731
+ ];
4732
+ onObjectChange?.({ object: target, position: next });
4733
+ onChange?.();
4734
+ };
4735
+ const handleUp = () => {
4736
+ window.removeEventListener("pointermove", handleMove);
4737
+ window.removeEventListener("pointerup", handleUp);
4738
+ window.removeEventListener("pointercancel", handleUp);
4739
+ const swallow = (e) => {
4740
+ e.stopPropagation();
4741
+ e.stopImmediatePropagation();
4742
+ };
4743
+ window.addEventListener("click", swallow, { capture: true, once: true });
4744
+ setTimeout(() => window.removeEventListener("click", swallow, true), 0);
4745
+ onMouseUp?.();
4746
+ onDraggingChanged?.(false);
4747
+ };
4748
+ window.addEventListener("pointermove", handleMove);
4749
+ window.addEventListener("pointerup", handleUp);
4750
+ window.addEventListener("pointercancel", handleUp);
4751
+ }
3128
4752
  function startRingDrag(opts) {
3129
4753
  const {
3130
4754
  cssAxis,
@@ -3147,6 +4771,7 @@ function startRingDrag(opts) {
3147
4771
  const startRotation = target.getRotation() ?? [0, 0, 0];
3148
4772
  onMouseDown?.();
3149
4773
  onDraggingChanged?.(true);
4774
+ const qStart = quatFromEulerXYZ(startRotation);
3150
4775
  const handleMove = (ev) => {
3151
4776
  const a = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
3152
4777
  let d = a - lastAngle;
@@ -3156,12 +4781,10 @@ function startRingDrag(opts) {
3156
4781
  lastAngle = a;
3157
4782
  let degrees = cumulative * 180 / Math.PI;
3158
4783
  degrees = snap(degrees, rotationSnap);
3159
- const newRotation = [
3160
- startRotation[0],
3161
- startRotation[1],
3162
- startRotation[2]
3163
- ];
3164
- newRotation[cssAxis] = startRotation[cssAxis] + degrees;
4784
+ const axisVec = [0, 0, 0];
4785
+ axisVec[cssAxis] = 1;
4786
+ const qDelta = quatFromAxisAngle(axisVec, degrees * Math.PI / 180);
4787
+ const newRotation = eulerXYZFromQuat(quatMultiply(qDelta, qStart));
3165
4788
  onObjectChange?.({ object: target, rotation: newRotation });
3166
4789
  onChange?.();
3167
4790
  };
@@ -3201,14 +4824,23 @@ function PolyTransformControls({
3201
4824
  onDraggingChanged
3202
4825
  }) {
3203
4826
  const [, forceRender] = useState4(0);
3204
- useEffect8(() => {
4827
+ useEffect9(() => {
3205
4828
  forceRender((n) => n + 1);
3206
4829
  }, [object]);
4830
+ const cameraCtxForRot = useContext4(PolyCameraContext);
4831
+ const rotX = useStoreSelector(
4832
+ cameraCtxForRot?.store ?? FALLBACK_CAMERA_STORE,
4833
+ (s) => s.cameraState.rotX
4834
+ );
4835
+ const rotY = useStoreSelector(
4836
+ cameraCtxForRot?.store ?? FALLBACK_CAMERA_STORE,
4837
+ (s) => s.cameraState.rotY
4838
+ );
3207
4839
  const [hoveredKey, setHoveredKey] = useState4(null);
3208
4840
  const [draggingKey, setDraggingKey] = useState4(null);
3209
4841
  const cameraCtx = useContext4(PolyCameraContext);
3210
4842
  const cameraElRef = cameraCtx?.cameraElRef;
3211
- const dragRef = useRef8({
4843
+ const dragRef = useRef9({
3212
4844
  target: null,
3213
4845
  mode: "translate",
3214
4846
  shaftLengthCss: 0,
@@ -3217,7 +4849,7 @@ function PolyTransformControls({
3217
4849
  translationSnap: null,
3218
4850
  rotationSnap: null
3219
4851
  });
3220
- useEffect8(() => {
4852
+ useEffect9(() => {
3221
4853
  const cameraEl = cameraElRef?.current;
3222
4854
  if (!cameraEl) return;
3223
4855
  const onPointerDown = (event) => {
@@ -3226,6 +4858,42 @@ function PolyTransformControls({
3226
4858
  const targetEl = event.target;
3227
4859
  if (targetEl?.closest(".polycss-transform-gizmo")) return;
3228
4860
  if (state.mode === "translate") {
4861
+ for (const spec of PLANE_SPECS) {
4862
+ const aL = ["x", "y", "z"][spec.axisA];
4863
+ const bL = ["x", "y", "z"][spec.axisB];
4864
+ if (!state.show[aL] || !state.show[bL]) continue;
4865
+ const planeEl = document.querySelector(
4866
+ `.polycss-transform-plane--${spec.key}`
4867
+ );
4868
+ if (!planeEl) continue;
4869
+ if (!pointInMeshElement(planeEl, event.clientX, event.clientY)) continue;
4870
+ event.preventDefault();
4871
+ event.stopPropagation();
4872
+ const wrapper = planeEl.closest(
4873
+ "[data-poly-transform-controls]"
4874
+ );
4875
+ if (!wrapper) return;
4876
+ setDraggingKey(spec.key);
4877
+ startPlaneDrag({
4878
+ axisA: spec.axisA,
4879
+ axisB: spec.axisB,
4880
+ probeDistanceCss: state.shaftLengthCss,
4881
+ wrapper,
4882
+ target: state.target,
4883
+ startClientX: event.clientX,
4884
+ startClientY: event.clientY,
4885
+ translationSnap: state.translationSnap,
4886
+ onChange: state.onChange,
4887
+ onObjectChange: state.onObjectChange,
4888
+ onMouseDown: state.onMouseDown,
4889
+ onMouseUp: state.onMouseUp,
4890
+ onDraggingChanged: (d) => {
4891
+ if (!d) setDraggingKey(null);
4892
+ state.onDraggingChanged?.(d);
4893
+ }
4894
+ });
4895
+ return;
4896
+ }
3229
4897
  for (const spec of ARROW_SPECS) {
3230
4898
  if (!state.show[userAxisLetterOf(spec.key)]) continue;
3231
4899
  const arrowEl = document.querySelector(
@@ -3325,9 +4993,9 @@ function PolyTransformControls({
3325
4993
  onDraggingChanged
3326
4994
  };
3327
4995
  const wrapperStyle = {
4996
+ transform: `translate3d(${wrapperPos[0]}px, ${wrapperPos[1]}px, ${wrapperPos[2]}px)`,
3328
4997
  position: "absolute",
3329
4998
  transformStyle: "preserve-3d",
3330
- transform: `translate3d(${wrapperPos[0]}px, ${wrapperPos[1]}px, ${wrapperPos[2]}px)`,
3331
4999
  // No `pointer-events: none` here — that property is inherited, so
3332
5000
  // setting it on the wrapper would cascade to every arrow polygon
3333
5001
  // and disable native hit-testing on the gizmo entirely. The
@@ -3335,7 +5003,7 @@ function PolyTransformControls({
3335
5003
  // own; descendants opt in via the default `auto`.
3336
5004
  zIndex: 1e3
3337
5005
  };
3338
- return /* @__PURE__ */ jsxs2(
5006
+ return /* @__PURE__ */ jsxs3(
3339
5007
  "div",
3340
5008
  {
3341
5009
  className: "polycss-transform-controls",
@@ -3350,7 +5018,8 @@ function PolyTransformControls({
3350
5018
  const hovered = hoveredKey === spec.key;
3351
5019
  const dragging = draggingKey === spec.key;
3352
5020
  const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
3353
- return /* @__PURE__ */ jsx7(
5021
+ const backFacing = isAxisBackFacing(spec.cssAxis, spec.sign, rotX, rotY);
5022
+ return /* @__PURE__ */ jsx8(
3354
5023
  TranslateArrow,
3355
5024
  {
3356
5025
  cssAxis: spec.cssAxis,
@@ -3358,6 +5027,41 @@ function PolyTransformControls({
3358
5027
  axisKey: spec.key,
3359
5028
  color: withAlpha(spec.color, alpha),
3360
5029
  shaftLengthCss,
5030
+ includeShaft: !backFacing,
5031
+ target,
5032
+ enabled,
5033
+ translationSnap,
5034
+ onChange,
5035
+ onObjectChange,
5036
+ onMouseDown,
5037
+ onMouseUp,
5038
+ onDraggingChanged,
5039
+ onHoverChange: (h) => setHoveredKey(h ? spec.key : (cur) => cur === spec.key ? null : cur),
5040
+ onDraggingStart: () => setDraggingKey(spec.key),
5041
+ onDraggingStop: () => setDraggingKey((cur) => cur === spec.key ? null : cur)
5042
+ },
5043
+ spec.key
5044
+ );
5045
+ }),
5046
+ mode === "translate" && PLANE_SPECS.map((spec) => {
5047
+ const aLetter = ["x", "y", "z"][spec.axisA];
5048
+ const bLetter = ["x", "y", "z"][spec.axisB];
5049
+ const show = { x: showX, y: showY, z: showZ }[aLetter] && { x: showX, y: showY, z: showZ }[bLetter];
5050
+ if (!show) return null;
5051
+ const hovered = hoveredKey === spec.key;
5052
+ const dragging = draggingKey === spec.key;
5053
+ const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
5054
+ return /* @__PURE__ */ jsx8(
5055
+ TranslatePlane,
5056
+ {
5057
+ axisA: spec.axisA,
5058
+ axisB: spec.axisB,
5059
+ perpAxis: spec.perpAxis,
5060
+ planeKey: spec.key,
5061
+ color: withAlpha(spec.color, alpha),
5062
+ shaftLengthCss,
5063
+ rotX,
5064
+ rotY,
3361
5065
  target,
3362
5066
  enabled,
3363
5067
  translationSnap,
@@ -3379,7 +5083,7 @@ function PolyTransformControls({
3379
5083
  const hovered = hoveredKey === spec.key;
3380
5084
  const dragging = draggingKey === spec.key;
3381
5085
  const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
3382
- return /* @__PURE__ */ jsx7(
5086
+ return /* @__PURE__ */ jsx8(
3383
5087
  RotateRing,
3384
5088
  {
3385
5089
  cssAxis: spec.cssAxis,
@@ -3411,6 +5115,7 @@ function TranslateArrow({
3411
5115
  axisKey,
3412
5116
  color,
3413
5117
  shaftLengthCss,
5118
+ includeShaft,
3414
5119
  target,
3415
5120
  enabled,
3416
5121
  translationSnap,
@@ -3423,7 +5128,7 @@ function TranslateArrow({
3423
5128
  onDraggingStart,
3424
5129
  onDraggingStop
3425
5130
  }) {
3426
- const cbRef = useRef8({
5131
+ const cbRef = useRef9({
3427
5132
  onChange,
3428
5133
  onObjectChange,
3429
5134
  onMouseDown,
@@ -3445,7 +5150,7 @@ function TranslateArrow({
3445
5150
  enabled,
3446
5151
  translationSnap
3447
5152
  };
3448
- const polygons = useMemo10(() => {
5153
+ const polygons = useMemo11(() => {
3449
5154
  const lengthWorld = shaftLengthCss / SCENE_TILE_SIZE;
3450
5155
  return arrowPolygons({
3451
5156
  axis: WORLD_AXIS_FOR_CSS[cssAxis],
@@ -3454,9 +5159,10 @@ function TranslateArrow({
3454
5159
  shaftHalfThickness: lengthWorld * SHAFT_HALF_THICKNESS_RATIO,
3455
5160
  headLength: lengthWorld * HEAD_LENGTH_RATIO,
3456
5161
  headHalfThickness: lengthWorld * HEAD_HALF_THICKNESS_RATIO,
3457
- color
5162
+ color,
5163
+ shaft: includeShaft
3458
5164
  });
3459
- }, [cssAxis, sign, color, shaftLengthCss]);
5165
+ }, [cssAxis, sign, color, shaftLengthCss, includeShaft]);
3460
5166
  const onPointerDown = useCallback7(
3461
5167
  (e) => {
3462
5168
  if (!cbRef.current.enabled) return;
@@ -3486,7 +5192,7 @@ function TranslateArrow({
3486
5192
  },
3487
5193
  [cssAxis, sign, target, shaftLengthCss]
3488
5194
  );
3489
- return /* @__PURE__ */ jsx7(
5195
+ return /* @__PURE__ */ jsx8(
3490
5196
  PolyMesh,
3491
5197
  {
3492
5198
  polygons,
@@ -3498,6 +5204,107 @@ function TranslateArrow({
3498
5204
  }
3499
5205
  );
3500
5206
  }
5207
+ function TranslatePlane({
5208
+ axisA,
5209
+ axisB,
5210
+ perpAxis,
5211
+ planeKey,
5212
+ color,
5213
+ shaftLengthCss,
5214
+ rotX,
5215
+ rotY,
5216
+ target,
5217
+ enabled,
5218
+ translationSnap,
5219
+ onChange,
5220
+ onObjectChange,
5221
+ onMouseDown,
5222
+ onMouseUp,
5223
+ onDraggingChanged,
5224
+ onHoverChange,
5225
+ onDraggingStart,
5226
+ onDraggingStop
5227
+ }) {
5228
+ const cbRef = useRef9({
5229
+ onChange,
5230
+ onObjectChange,
5231
+ onMouseDown,
5232
+ onMouseUp,
5233
+ onDraggingChanged,
5234
+ onDraggingStart,
5235
+ onDraggingStop,
5236
+ enabled,
5237
+ translationSnap
5238
+ });
5239
+ cbRef.current = {
5240
+ onChange,
5241
+ onObjectChange,
5242
+ onMouseDown,
5243
+ onMouseUp,
5244
+ onDraggingChanged,
5245
+ onDraggingStart,
5246
+ onDraggingStop,
5247
+ enabled,
5248
+ translationSnap
5249
+ };
5250
+ const polygons = useMemo11(() => {
5251
+ const lengthWorld = shaftLengthCss / SCENE_TILE_SIZE;
5252
+ const worldPerp = WORLD_AXIS_FOR_CSS[perpAxis];
5253
+ const worldA = (worldPerp + 1) % 3;
5254
+ const worldB = (worldPerp + 2) % 3;
5255
+ const cssAForOffset = WORLD_AXIS_FOR_CSS[worldA];
5256
+ const cssBForOffset = WORLD_AXIS_FOR_CSS[worldB];
5257
+ const signA = isAxisBackFacing(cssAForOffset, 1, rotX, rotY) ? -1 : 1;
5258
+ const signB = isAxisBackFacing(cssBForOffset, 1, rotX, rotY) ? -1 : 1;
5259
+ const mag = lengthWorld * PLANE_OFFSET_RATIO;
5260
+ return planePolygons({
5261
+ axis: worldPerp,
5262
+ size: lengthWorld * PLANE_HALF_SIZE_RATIO,
5263
+ offset: [signA * mag, signB * mag],
5264
+ color
5265
+ });
5266
+ }, [perpAxis, color, shaftLengthCss, rotX, rotY]);
5267
+ const onPointerDown = useCallback7(
5268
+ (e) => {
5269
+ if (!cbRef.current.enabled) return;
5270
+ e.stopPropagation();
5271
+ const meshEl = e.eventObject.element;
5272
+ const wrapper = meshEl?.closest("[data-poly-transform-controls]");
5273
+ if (!wrapper) return;
5274
+ cbRef.current.onDraggingStart?.();
5275
+ startPlaneDrag({
5276
+ axisA,
5277
+ axisB,
5278
+ probeDistanceCss: shaftLengthCss,
5279
+ wrapper,
5280
+ target,
5281
+ startClientX: e.nativeEvent.clientX,
5282
+ startClientY: e.nativeEvent.clientY,
5283
+ translationSnap: cbRef.current.translationSnap,
5284
+ onChange: cbRef.current.onChange,
5285
+ onObjectChange: cbRef.current.onObjectChange,
5286
+ onMouseDown: cbRef.current.onMouseDown,
5287
+ onMouseUp: cbRef.current.onMouseUp,
5288
+ onDraggingChanged: (d) => {
5289
+ if (!d) cbRef.current.onDraggingStop?.();
5290
+ cbRef.current.onDraggingChanged?.(d);
5291
+ }
5292
+ });
5293
+ },
5294
+ [axisA, axisB, target, shaftLengthCss]
5295
+ );
5296
+ return /* @__PURE__ */ jsx8(
5297
+ PolyMesh,
5298
+ {
5299
+ polygons,
5300
+ onPointerDown,
5301
+ onPointerOver: () => onHoverChange?.(true),
5302
+ onPointerOut: () => onHoverChange?.(false),
5303
+ className: `polycss-transform-gizmo polycss-transform-plane polycss-transform-plane--${planeKey}`,
5304
+ textureLighting: "baked"
5305
+ }
5306
+ );
5307
+ }
3501
5308
  function RotateRing({
3502
5309
  cssAxis,
3503
5310
  axisKey,
@@ -3515,7 +5322,7 @@ function RotateRing({
3515
5322
  onDraggingStart,
3516
5323
  onDraggingStop
3517
5324
  }) {
3518
- const cbRef = useRef8({
5325
+ const cbRef = useRef9({
3519
5326
  onChange,
3520
5327
  onObjectChange,
3521
5328
  onMouseDown,
@@ -3537,16 +5344,17 @@ function RotateRing({
3537
5344
  enabled,
3538
5345
  rotationSnap
3539
5346
  };
3540
- const polygons = useMemo10(() => {
5347
+ const polygons = useMemo11(() => {
3541
5348
  const radiusWorld = radiusCss / SCENE_TILE_SIZE;
3542
- return ringPolygons({
5349
+ const outerWorld = radiusWorld * RING_QUAD_OUTER_RATIO;
5350
+ return ringQuadPolygons({
3543
5351
  axis: WORLD_AXIS_FOR_CSS[cssAxis],
3544
- radius: radiusWorld,
3545
- halfThickness: radiusWorld * RING_HALF_THICKNESS_RATIO,
3546
- segments: RING_SEGMENTS,
5352
+ outerRadius: outerWorld,
3547
5353
  color
3548
5354
  });
3549
5355
  }, [cssAxis, color, radiusCss]);
5356
+ const ringInnerRatio = (1 - RING_HALF_THICKNESS_RATIO) / RING_QUAD_OUTER_RATIO;
5357
+ const ringOuterRatio = (1 + RING_HALF_THICKNESS_RATIO) / RING_QUAD_OUTER_RATIO;
3550
5358
  const onPointerDown = useCallback7(
3551
5359
  (e) => {
3552
5360
  if (!cbRef.current.enabled) return;
@@ -3574,7 +5382,7 @@ function RotateRing({
3574
5382
  },
3575
5383
  [cssAxis, target]
3576
5384
  );
3577
- return /* @__PURE__ */ jsx7(
5385
+ return /* @__PURE__ */ jsx8(
3578
5386
  PolyMesh,
3579
5387
  {
3580
5388
  polygons,
@@ -3582,6 +5390,10 @@ function RotateRing({
3582
5390
  onPointerOver: () => onHoverChange?.(true),
3583
5391
  onPointerOut: () => onHoverChange?.(false),
3584
5392
  className: `polycss-transform-gizmo polycss-transform-ring polycss-transform-ring--${axisKey}`,
5393
+ style: {
5394
+ ["--ring-inner-ratio"]: ringInnerRatio,
5395
+ ["--ring-outer-ratio"]: ringOuterRatio
5396
+ },
3585
5397
  textureLighting: "baked"
3586
5398
  }
3587
5399
  );
@@ -3592,12 +5404,12 @@ import {
3592
5404
  createContext as createContext3,
3593
5405
  useCallback as useCallback8,
3594
5406
  useContext as useContext5,
3595
- useEffect as useEffect9,
3596
- useMemo as useMemo11,
3597
- useRef as useRef9,
5407
+ useEffect as useEffect10,
5408
+ useMemo as useMemo12,
5409
+ useRef as useRef10,
3598
5410
  useState as useState5
3599
5411
  } from "react";
3600
- import { jsx as jsx8 } from "react/jsx-runtime";
5412
+ import { jsx as jsx9 } from "react/jsx-runtime";
3601
5413
  var SelectContext = createContext3(null);
3602
5414
  function PolySelect({
3603
5415
  multiple = false,
@@ -3610,18 +5422,18 @@ function PolySelect({
3610
5422
  style
3611
5423
  }) {
3612
5424
  const [selected, setSelectedState] = useState5([]);
3613
- const filterRef = useRef9(filter);
5425
+ const filterRef = useRef10(filter);
3614
5426
  filterRef.current = filter;
3615
- const onChangeRef = useRef9(onChange);
5427
+ const onChangeRef = useRef10(onChange);
3616
5428
  onChangeRef.current = onChange;
3617
- const multipleRef = useRef9(multiple);
5429
+ const multipleRef = useRef10(multiple);
3618
5430
  multipleRef.current = multiple;
3619
5431
  const apply = useCallback8((next) => {
3620
5432
  const filtered = filterRef.current ? filterRef.current(next) : next;
3621
5433
  setSelectedState(filtered);
3622
5434
  if (onChangeRef.current) onChangeRef.current(filtered);
3623
5435
  }, []);
3624
- const api = useMemo11(() => {
5436
+ const api = useMemo12(() => {
3625
5437
  return {
3626
5438
  selected,
3627
5439
  set: (next) => apply(next),
@@ -3654,12 +5466,12 @@ function PolySelect({
3654
5466
  has: (h) => selected.includes(h)
3655
5467
  };
3656
5468
  }, [selected, apply]);
3657
- const wrapperRef = useRef9(null);
5469
+ const wrapperRef = useRef10(null);
3658
5470
  const cameraCtx = useContext5(PolyCameraContext);
3659
5471
  const cameraElRef = cameraCtx?.cameraElRef;
3660
- const clearOnMissRef = useRef9(clearOnMiss);
5472
+ const clearOnMissRef = useRef10(clearOnMiss);
3661
5473
  clearOnMissRef.current = clearOnMiss;
3662
- const onPointerMissedRef = useRef9(onPointerMissed);
5474
+ const onPointerMissedRef = useRef10(onPointerMissed);
3663
5475
  onPointerMissedRef.current = onPointerMissed;
3664
5476
  const findMeshUnderPoint2 = useCallback8(
3665
5477
  (clientX, clientY) => findMeshUnderPoint(
@@ -3669,7 +5481,7 @@ function PolySelect({
3669
5481
  ),
3670
5482
  []
3671
5483
  );
3672
- useEffect9(() => {
5484
+ useEffect10(() => {
3673
5485
  const cameraEl = cameraElRef?.current;
3674
5486
  if (!cameraEl) return;
3675
5487
  const onClick = (event) => {
@@ -3716,7 +5528,7 @@ function PolySelect({
3716
5528
  }
3717
5529
  };
3718
5530
  const wrapperStyle = { display: "contents", ...style };
3719
- return /* @__PURE__ */ jsx8(SelectContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(
5531
+ return /* @__PURE__ */ jsx9(SelectContext.Provider, { value: api, children: /* @__PURE__ */ jsx9(
3720
5532
  "div",
3721
5533
  {
3722
5534
  ref: wrapperRef,
@@ -3740,9 +5552,9 @@ function usePolySelectionApi() {
3740
5552
  }
3741
5553
 
3742
5554
  // src/helpers/PolyAxesHelper.tsx
3743
- import { useMemo as useMemo12 } from "react";
5555
+ import { useMemo as useMemo13 } from "react";
3744
5556
  import { axesHelperPolygons } from "@layoutit/polycss-core";
3745
- import { jsx as jsx9 } from "react/jsx-runtime";
5557
+ import { jsx as jsx10 } from "react/jsx-runtime";
3746
5558
  function PolyAxesHelper({
3747
5559
  size,
3748
5560
  thickness,
@@ -3751,17 +5563,17 @@ function PolyAxesHelper({
3751
5563
  yColor,
3752
5564
  zColor
3753
5565
  }) {
3754
- const polygons = useMemo12(
5566
+ const polygons = useMemo13(
3755
5567
  () => axesHelperPolygons({ size, thickness, negative, xColor, yColor, zColor }),
3756
5568
  [size, thickness, negative, xColor, yColor, zColor]
3757
5569
  );
3758
- return /* @__PURE__ */ jsx9(PolyMesh, { polygons });
5570
+ return /* @__PURE__ */ jsx10(PolyMesh, { polygons });
3759
5571
  }
3760
5572
 
3761
5573
  // src/helpers/PolyDirectionalLightHelper.tsx
3762
- import { useMemo as useMemo13 } from "react";
5574
+ import { useMemo as useMemo14 } from "react";
3763
5575
  import { octahedronPolygons } from "@layoutit/polycss-core";
3764
- import { jsx as jsx10 } from "react/jsx-runtime";
5576
+ import { jsx as jsx11 } from "react/jsx-runtime";
3765
5577
  var TILE = 50;
3766
5578
  function PolyDirectionalLightHelper({
3767
5579
  light,
@@ -3771,11 +5583,11 @@ function PolyDirectionalLightHelper({
3771
5583
  color
3772
5584
  }) {
3773
5585
  const swatch = color ?? light.color ?? "#ffd54a";
3774
- const polygons = useMemo13(
5586
+ const polygons = useMemo14(
3775
5587
  () => octahedronPolygons({ center: [0, 0, 0], size, color: swatch }),
3776
5588
  [size, swatch]
3777
5589
  );
3778
- const meshPosition = useMemo13(() => {
5590
+ const meshPosition = useMemo14(() => {
3779
5591
  const [dx, dy, dz] = light.direction;
3780
5592
  const len = Math.hypot(dx, dy, dz) || 1;
3781
5593
  const tx = target?.[0] ?? 0;
@@ -3786,14 +5598,14 @@ function PolyDirectionalLightHelper({
3786
5598
  const worldZ = tz + dz / len * distance;
3787
5599
  return [worldY * TILE, worldX * TILE, worldZ * TILE];
3788
5600
  }, [light.direction, target, distance]);
3789
- return /* @__PURE__ */ jsx10(PolyMesh, { polygons, position: meshPosition });
5601
+ return /* @__PURE__ */ jsx11(PolyMesh, { polygons, position: meshPosition });
3790
5602
  }
3791
5603
 
3792
5604
  // src/styles/colorResolver.ts
3793
5605
  import { parsePureColor as parsePureColor2 } from "@layoutit/polycss-core";
3794
5606
 
3795
5607
  // src/animation/usePolyAnimation.ts
3796
- import { useEffect as useEffect10, useRef as useRef10, useMemo as useMemo14 } from "react";
5608
+ import { useEffect as useEffect11, useRef as useRef11, useMemo as useMemo15 } from "react";
3797
5609
  import { createPolyAnimationMixer } from "@layoutit/polycss-core";
3798
5610
  function resolveRoot(rootArg) {
3799
5611
  if (!rootArg) return null;
@@ -3801,9 +5613,9 @@ function resolveRoot(rootArg) {
3801
5613
  return rootArg;
3802
5614
  }
3803
5615
  function usePolyAnimation(clips, controller, root) {
3804
- const internalRef = useRef10(null);
3805
- const mixerRef = useRef10(null);
3806
- useEffect10(() => {
5616
+ const internalRef = useRef11(null);
5617
+ const mixerRef = useRef11(null);
5618
+ useEffect11(() => {
3807
5619
  if (!clips || clips.length === 0 || !controller) {
3808
5620
  mixerRef.current = null;
3809
5621
  return;
@@ -3820,7 +5632,7 @@ function usePolyAnimation(clips, controller, root) {
3820
5632
  mixerRef.current = null;
3821
5633
  };
3822
5634
  }, [clips, controller]);
3823
- useEffect10(() => {
5635
+ useEffect11(() => {
3824
5636
  if (!clips || clips.length === 0 || !controller) return;
3825
5637
  let rafId;
3826
5638
  let lastTime = null;
@@ -3843,7 +5655,7 @@ function usePolyAnimation(clips, controller, root) {
3843
5655
  }, [clips, controller]);
3844
5656
  const resolvedClips = clips ?? [];
3845
5657
  const resolvedNames = resolvedClips.map((c) => c.name);
3846
- const actions = useMemo14(() => {
5658
+ const actions = useMemo15(() => {
3847
5659
  const target = {};
3848
5660
  for (const clip of resolvedClips) {
3849
5661
  Object.defineProperty(target, clip.name, {
@@ -3872,24 +5684,38 @@ function usePolyAnimation(clips, controller, root) {
3872
5684
 
3873
5685
  // src/index.ts
3874
5686
  import {
5687
+ CAMERA_BACKFACE_CULL_EPS,
5688
+ VOXEL_CAMERA_CULL_AXIS_EPS,
5689
+ VOXEL_CAMERA_CULL_NORMAL_LIMIT,
3875
5690
  normalizePolygons,
3876
5691
  mergePolygons as mergePolygons2,
3877
5692
  coverPlanarPolygons,
5693
+ optimizeMeshPolygons,
3878
5694
  cullInteriorPolygons,
5695
+ cameraCullNormalGroups,
5696
+ cameraCullNormalGroupsFromPolygons,
5697
+ cameraCullNormalKey,
5698
+ cameraCullVisibleSignature,
5699
+ cameraFacingDepth,
5700
+ isAxisAlignedSurfaceNormal,
5701
+ isVoxelCameraCullableNormalGroups,
5702
+ normalFacesCamera,
5703
+ polygonCssSurfaceNormal,
5704
+ polygonFacesCamera,
3879
5705
  parseObj,
3880
5706
  parseMtl,
3881
5707
  parseGltf,
3882
5708
  bakeSolidTextureSamples,
3883
5709
  bakeSolidTextureSampledPolygons,
3884
5710
  loadMesh as loadMesh2,
3885
- createIsometricCamera as createIsometricCamera3,
5711
+ createIsometricCamera as createIsometricCamera2,
3886
5712
  parseVox,
3887
5713
  polygonFaces,
3888
5714
  computeTexturePaintMetrics,
3889
5715
  computeShapeLighting,
3890
5716
  parseColor,
3891
5717
  parsePureColor as parsePureColor3,
3892
- parseHexColor as parseHexColor2,
5718
+ parseHexColor as parseHexColor3,
3893
5719
  parseRgbColor,
3894
5720
  formatColor,
3895
5721
  clampChannel,
@@ -3897,13 +5723,14 @@ import {
3897
5723
  rotateVec3,
3898
5724
  inverseRotateVec3 as inverseRotateVec32,
3899
5725
  axesHelperPolygons as axesHelperPolygons2,
5726
+ boxPolygons,
3900
5727
  arrowPolygons as arrowPolygons2,
3901
- ringPolygons as ringPolygons2,
5728
+ ringPolygons,
3902
5729
  octahedronPolygons as octahedronPolygons2,
3903
5730
  buildSceneContext as buildSceneContext2,
3904
5731
  computeSceneBbox as computeSceneBbox2,
3905
- BASE_TILE as BASE_TILE3,
3906
- DEFAULT_CAMERA_STATE,
5732
+ BASE_TILE as BASE_TILE5,
5733
+ DEFAULT_CAMERA_STATE as DEFAULT_CAMERA_STATE2,
3907
5734
  DEFAULT_PROJECTION,
3908
5735
  normalizeInvertMultiplier,
3909
5736
  createPolyAnimationMixer as createPolyAnimationMixer2,
@@ -3912,8 +5739,9 @@ import {
3912
5739
  LoopPingPong
3913
5740
  } from "@layoutit/polycss-core";
3914
5741
  export {
3915
- BASE_TILE3 as BASE_TILE,
3916
- DEFAULT_CAMERA_STATE,
5742
+ BASE_TILE5 as BASE_TILE,
5743
+ CAMERA_BACKFACE_CULL_EPS,
5744
+ DEFAULT_CAMERA_STATE2 as DEFAULT_CAMERA_STATE,
3917
5745
  DEFAULT_PROJECTION,
3918
5746
  LoopOnce,
3919
5747
  LoopPingPong,
@@ -3923,6 +5751,8 @@ export {
3923
5751
  PolyPerspectiveCamera as PolyCamera,
3924
5752
  PolyCameraContext,
3925
5753
  PolyDirectionalLightHelper,
5754
+ PolyFirstPersonControls,
5755
+ PolyGround,
3926
5756
  PolyMapControls,
3927
5757
  PolyMesh,
3928
5758
  PolyOrbitControls,
@@ -3931,17 +5761,25 @@ export {
3931
5761
  PolyScene,
3932
5762
  PolySelect,
3933
5763
  PolyTransformControls,
5764
+ VOXEL_CAMERA_CULL_AXIS_EPS,
5765
+ VOXEL_CAMERA_CULL_NORMAL_LIMIT,
3934
5766
  arrowPolygons2 as arrowPolygons,
3935
5767
  axesHelperPolygons2 as axesHelperPolygons,
3936
5768
  bakeSolidTextureSampledPolygons,
3937
5769
  bakeSolidTextureSamples,
5770
+ boxPolygons,
3938
5771
  buildSceneContext2 as buildSceneContext,
5772
+ cameraCullNormalGroups,
5773
+ cameraCullNormalGroupsFromPolygons,
5774
+ cameraCullNormalKey,
5775
+ cameraCullVisibleSignature,
5776
+ cameraFacingDepth,
3939
5777
  clampChannel,
3940
5778
  computeSceneBbox2 as computeSceneBbox,
3941
5779
  computeShapeLighting,
3942
5780
  computeTexturePaintMetrics,
3943
5781
  coverPlanarPolygons,
3944
- createIsometricCamera3 as createIsometricCamera,
5782
+ createIsometricCamera2 as createIsometricCamera,
3945
5783
  createPolyAnimationMixer2 as createPolyAnimationMixer,
3946
5784
  cullInteriorPolygons,
3947
5785
  findMeshUnderPoint,
@@ -3949,22 +5787,28 @@ export {
3949
5787
  formatColor,
3950
5788
  injectPolyBaseStyles,
3951
5789
  inverseRotateVec32 as inverseRotateVec3,
5790
+ isAxisAlignedSurfaceNormal,
5791
+ isVoxelCameraCullableNormalGroups,
3952
5792
  loadMesh2 as loadMesh,
3953
5793
  mergePolygons2 as mergePolygons,
5794
+ normalFacesCamera,
3954
5795
  normalizeInvertMultiplier,
3955
5796
  normalizePolygons,
3956
5797
  octahedronPolygons2 as octahedronPolygons,
5798
+ optimizeMeshPolygons,
3957
5799
  parseColor,
3958
5800
  parseGltf,
3959
- parseHexColor2 as parseHexColor,
5801
+ parseHexColor3 as parseHexColor,
3960
5802
  parseMtl,
3961
5803
  parseObj,
3962
5804
  parsePureColor3 as parsePureColor,
3963
5805
  parseRgbColor,
3964
5806
  parseVox,
3965
5807
  pointInMeshElement,
5808
+ polygonCssSurfaceNormal,
3966
5809
  polygonFaces,
3967
- ringPolygons2 as ringPolygons,
5810
+ polygonFacesCamera,
5811
+ ringPolygons,
3968
5812
  rotateVec3,
3969
5813
  shadeColor,
3970
5814
  useCameraContext,