@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.cjs CHANGED
@@ -20,17 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
- BASE_TILE: () => import_polycss_core13.BASE_TILE,
24
- DEFAULT_CAMERA_STATE: () => import_polycss_core13.DEFAULT_CAMERA_STATE,
25
- DEFAULT_PROJECTION: () => import_polycss_core13.DEFAULT_PROJECTION,
26
- LoopOnce: () => import_polycss_core13.LoopOnce,
27
- LoopPingPong: () => import_polycss_core13.LoopPingPong,
28
- LoopRepeat: () => import_polycss_core13.LoopRepeat,
23
+ BASE_TILE: () => import_polycss_core14.BASE_TILE,
24
+ CAMERA_BACKFACE_CULL_EPS: () => import_polycss_core14.CAMERA_BACKFACE_CULL_EPS,
25
+ DEFAULT_CAMERA_STATE: () => import_polycss_core14.DEFAULT_CAMERA_STATE,
26
+ DEFAULT_PROJECTION: () => import_polycss_core14.DEFAULT_PROJECTION,
27
+ LoopOnce: () => import_polycss_core14.LoopOnce,
28
+ LoopPingPong: () => import_polycss_core14.LoopPingPong,
29
+ LoopRepeat: () => import_polycss_core14.LoopRepeat,
29
30
  Poly: () => Poly,
30
31
  PolyAxesHelper: () => PolyAxesHelper,
31
32
  PolyCamera: () => PolyPerspectiveCamera,
32
33
  PolyCameraContext: () => PolyCameraContext,
33
34
  PolyDirectionalLightHelper: () => PolyDirectionalLightHelper,
35
+ PolyFirstPersonControls: () => PolyFirstPersonControls,
36
+ PolyGround: () => PolyGround,
34
37
  PolyMapControls: () => PolyMapControls,
35
38
  PolyMesh: () => PolyMesh,
36
39
  PolyOrbitControls: () => PolyOrbitControls,
@@ -39,42 +42,56 @@ __export(src_exports, {
39
42
  PolyScene: () => PolyScene,
40
43
  PolySelect: () => PolySelect,
41
44
  PolyTransformControls: () => PolyTransformControls,
42
- arrowPolygons: () => import_polycss_core13.arrowPolygons,
43
- axesHelperPolygons: () => import_polycss_core13.axesHelperPolygons,
44
- bakeSolidTextureSampledPolygons: () => import_polycss_core13.bakeSolidTextureSampledPolygons,
45
- bakeSolidTextureSamples: () => import_polycss_core13.bakeSolidTextureSamples,
46
- buildSceneContext: () => import_polycss_core13.buildSceneContext,
47
- clampChannel: () => import_polycss_core13.clampChannel,
48
- computeSceneBbox: () => import_polycss_core13.computeSceneBbox,
49
- computeShapeLighting: () => import_polycss_core13.computeShapeLighting,
50
- computeTexturePaintMetrics: () => import_polycss_core13.computeTexturePaintMetrics,
51
- coverPlanarPolygons: () => import_polycss_core13.coverPlanarPolygons,
52
- createIsometricCamera: () => import_polycss_core13.createIsometricCamera,
53
- createPolyAnimationMixer: () => import_polycss_core13.createPolyAnimationMixer,
54
- cullInteriorPolygons: () => import_polycss_core13.cullInteriorPolygons,
45
+ VOXEL_CAMERA_CULL_AXIS_EPS: () => import_polycss_core14.VOXEL_CAMERA_CULL_AXIS_EPS,
46
+ VOXEL_CAMERA_CULL_NORMAL_LIMIT: () => import_polycss_core14.VOXEL_CAMERA_CULL_NORMAL_LIMIT,
47
+ arrowPolygons: () => import_polycss_core14.arrowPolygons,
48
+ axesHelperPolygons: () => import_polycss_core14.axesHelperPolygons,
49
+ bakeSolidTextureSampledPolygons: () => import_polycss_core14.bakeSolidTextureSampledPolygons,
50
+ bakeSolidTextureSamples: () => import_polycss_core14.bakeSolidTextureSamples,
51
+ boxPolygons: () => import_polycss_core14.boxPolygons,
52
+ buildSceneContext: () => import_polycss_core14.buildSceneContext,
53
+ cameraCullNormalGroups: () => import_polycss_core14.cameraCullNormalGroups,
54
+ cameraCullNormalGroupsFromPolygons: () => import_polycss_core14.cameraCullNormalGroupsFromPolygons,
55
+ cameraCullNormalKey: () => import_polycss_core14.cameraCullNormalKey,
56
+ cameraCullVisibleSignature: () => import_polycss_core14.cameraCullVisibleSignature,
57
+ cameraFacingDepth: () => import_polycss_core14.cameraFacingDepth,
58
+ clampChannel: () => import_polycss_core14.clampChannel,
59
+ computeSceneBbox: () => import_polycss_core14.computeSceneBbox,
60
+ computeShapeLighting: () => import_polycss_core14.computeShapeLighting,
61
+ computeTexturePaintMetrics: () => import_polycss_core14.computeTexturePaintMetrics,
62
+ coverPlanarPolygons: () => import_polycss_core14.coverPlanarPolygons,
63
+ createIsometricCamera: () => import_polycss_core14.createIsometricCamera,
64
+ createPolyAnimationMixer: () => import_polycss_core14.createPolyAnimationMixer,
65
+ cullInteriorPolygons: () => import_polycss_core14.cullInteriorPolygons,
55
66
  findMeshUnderPoint: () => findMeshUnderPoint,
56
67
  findPolyMeshHandle: () => findPolyMeshHandle,
57
- formatColor: () => import_polycss_core13.formatColor,
68
+ formatColor: () => import_polycss_core14.formatColor,
58
69
  injectPolyBaseStyles: () => injectPolyBaseStyles,
59
- inverseRotateVec3: () => import_polycss_core13.inverseRotateVec3,
60
- loadMesh: () => import_polycss_core13.loadMesh,
61
- mergePolygons: () => import_polycss_core13.mergePolygons,
62
- normalizeInvertMultiplier: () => import_polycss_core13.normalizeInvertMultiplier,
63
- normalizePolygons: () => import_polycss_core13.normalizePolygons,
64
- octahedronPolygons: () => import_polycss_core13.octahedronPolygons,
65
- parseColor: () => import_polycss_core13.parseColor,
66
- parseGltf: () => import_polycss_core13.parseGltf,
67
- parseHexColor: () => import_polycss_core13.parseHexColor,
68
- parseMtl: () => import_polycss_core13.parseMtl,
69
- parseObj: () => import_polycss_core13.parseObj,
70
- parsePureColor: () => import_polycss_core13.parsePureColor,
71
- parseRgbColor: () => import_polycss_core13.parseRgbColor,
72
- parseVox: () => import_polycss_core13.parseVox,
70
+ inverseRotateVec3: () => import_polycss_core14.inverseRotateVec3,
71
+ isAxisAlignedSurfaceNormal: () => import_polycss_core14.isAxisAlignedSurfaceNormal,
72
+ isVoxelCameraCullableNormalGroups: () => import_polycss_core14.isVoxelCameraCullableNormalGroups,
73
+ loadMesh: () => import_polycss_core14.loadMesh,
74
+ mergePolygons: () => import_polycss_core14.mergePolygons,
75
+ normalFacesCamera: () => import_polycss_core14.normalFacesCamera,
76
+ normalizeInvertMultiplier: () => import_polycss_core14.normalizeInvertMultiplier,
77
+ normalizePolygons: () => import_polycss_core14.normalizePolygons,
78
+ octahedronPolygons: () => import_polycss_core14.octahedronPolygons,
79
+ optimizeMeshPolygons: () => import_polycss_core14.optimizeMeshPolygons,
80
+ parseColor: () => import_polycss_core14.parseColor,
81
+ parseGltf: () => import_polycss_core14.parseGltf,
82
+ parseHexColor: () => import_polycss_core14.parseHexColor,
83
+ parseMtl: () => import_polycss_core14.parseMtl,
84
+ parseObj: () => import_polycss_core14.parseObj,
85
+ parsePureColor: () => import_polycss_core14.parsePureColor,
86
+ parseRgbColor: () => import_polycss_core14.parseRgbColor,
87
+ parseVox: () => import_polycss_core14.parseVox,
73
88
  pointInMeshElement: () => pointInMeshElement,
74
- polygonFaces: () => import_polycss_core13.polygonFaces,
75
- ringPolygons: () => import_polycss_core13.ringPolygons,
76
- rotateVec3: () => import_polycss_core13.rotateVec3,
77
- shadeColor: () => import_polycss_core13.shadeColor,
89
+ polygonCssSurfaceNormal: () => import_polycss_core14.polygonCssSurfaceNormal,
90
+ polygonFaces: () => import_polycss_core14.polygonFaces,
91
+ polygonFacesCamera: () => import_polycss_core14.polygonFacesCamera,
92
+ ringPolygons: () => import_polycss_core14.ringPolygons,
93
+ rotateVec3: () => import_polycss_core14.rotateVec3,
94
+ shadeColor: () => import_polycss_core14.shadeColor,
78
95
  useCameraContext: () => useCameraContext,
79
96
  usePolyAnimation: () => usePolyAnimation,
80
97
  usePolyCamera: () => usePolyCamera,
@@ -108,7 +125,8 @@ var import_polycss_core = require("@layoutit/polycss-core");
108
125
  var import_react2 = require("react");
109
126
  function createSceneStore(initial) {
110
127
  let state = {
111
- cameraState: { ...initial }
128
+ cameraState: { ...initial },
129
+ autoCenterOffset: [0, 0, 0]
112
130
  };
113
131
  const listeners = /* @__PURE__ */ new Set();
114
132
  function notify() {
@@ -127,15 +145,24 @@ function createSceneStore(initial) {
127
145
  return () => listeners.delete(listener);
128
146
  },
129
147
  updateCameraFromRef(handle) {
130
- state = { cameraState: { ...handle.state } };
148
+ state = { ...state, cameraState: { ...handle.state } };
131
149
  notify();
132
150
  return true;
133
151
  },
134
152
  notifyAll() {
135
153
  notify();
154
+ },
155
+ setAutoCenterOffset(offset) {
156
+ state = { ...state, autoCenterOffset: offset };
136
157
  }
137
158
  };
138
159
  }
160
+ function useStoreSelector(store, selector) {
161
+ const selectorRef = (0, import_react2.useRef)(selector);
162
+ selectorRef.current = selector;
163
+ const getSnapshot = (0, import_react2.useCallback)(() => selectorRef.current(store.getState()), [store]);
164
+ return (0, import_react2.useSyncExternalStore)(store.subscribe, getSnapshot);
165
+ }
139
166
 
140
167
  // src/camera/useCamera.ts
141
168
  function usePolyCamera(options) {
@@ -168,11 +195,14 @@ function usePolyCamera(options) {
168
195
  const el = sceneElRef.current;
169
196
  if (el) {
170
197
  const s = handle.state;
198
+ const [ox, oy, oz] = store.getState().autoCenterOffset;
171
199
  const tileSize = import_polycss_core.BASE_TILE;
172
- const [tx, ty, tz] = s.target;
173
- const cssX = ty * tileSize;
174
- const cssY = tx * tileSize;
175
- const cssZ = tz * tileSize;
200
+ const wx = s.target[0] + ox;
201
+ const wy = s.target[1] + oy;
202
+ const wz = s.target[2] + oz;
203
+ const cssX = wy * tileSize;
204
+ const cssY = wx * tileSize;
205
+ const cssZ = wz * tileSize;
176
206
  const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
177
207
  el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
178
208
  }
@@ -185,14 +215,17 @@ function usePolyCamera(options) {
185
215
  if (!el) return;
186
216
  const handle = handleRef.current;
187
217
  const s = handle.state;
218
+ const [ox, oy, oz] = store.getState().autoCenterOffset;
188
219
  const tileSize = import_polycss_core.BASE_TILE;
189
- const [tx, ty, tz] = s.target;
190
- const cssX = ty * tileSize;
191
- const cssY = tx * tileSize;
192
- const cssZ = tz * tileSize;
220
+ const wx = s.target[0] + ox;
221
+ const wy = s.target[1] + oy;
222
+ const wz = s.target[2] + oz;
223
+ const cssX = wy * tileSize;
224
+ const cssY = wx * tileSize;
225
+ const cssZ = wz * tileSize;
193
226
  const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
194
227
  el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
195
- }, []);
228
+ }, [store]);
196
229
  return {
197
230
  store,
198
231
  cameraRef: handleRef,
@@ -330,12 +363,6 @@ var CORE_BASE_STYLES = `
330
363
  height: 0;
331
364
  transform-style: preserve-3d;
332
365
  perspective: none;
333
- transform: var(--scene-transform);
334
- }
335
-
336
- .polycss-offset {
337
- transform-style: preserve-3d;
338
- transform: var(--offset-transform);
339
366
  }
340
367
 
341
368
  /* \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 */
@@ -358,6 +385,22 @@ var CORE_BASE_STYLES = `
358
385
  position: absolute;
359
386
  }
360
387
 
388
+ /* \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 */
389
+
390
+ /* PolyFirstPersonControls toggles this class on its host element (the camera
391
+ wrapper). FPV needs a real perspective context so scene Z translation
392
+ produces visible depth motion - without it, walking forward looks like a
393
+ planar pan. The class wins over inline perspective styles (e.g.
394
+ PolyOrthographicCamera's perspective: none) via !important. The actual
395
+ perspective value is set inline by the controls as the
396
+ --polycss-fpv-perspective custom property; the default of 2000px matches
397
+ the controls' lookOffset fallback so the FPV math and visual perspective
398
+ stay in sync. */
399
+ .polycss-fpv-host {
400
+ perspective: var(--polycss-fpv-perspective, 2000px) !important;
401
+ transform-style: preserve-3d !important;
402
+ }
403
+
361
404
  /* \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 */
362
405
 
363
406
  /*
@@ -385,24 +428,37 @@ var CORE_BASE_STYLES = `
385
428
  background-repeat: no-repeat;
386
429
  }
387
430
 
431
+ .polycss-scene b,
432
+ .polycss-scene i,
433
+ .polycss-scene u {
434
+ color: var(--polycss-paint, currentColor);
435
+ }
436
+
388
437
  .polycss-scene b {
438
+ background: currentColor;
439
+ width: 64px;
440
+ height: 64px;
389
441
  }
390
442
 
391
443
  .polycss-scene i {
392
- border-style: solid;
393
- border-width: 1px;
444
+ width: 16px;
445
+ height: 16px;
394
446
  border-color: currentColor;
395
447
  }
396
448
 
397
449
  .polycss-scene s {
450
+ width: var(--polycss-atlas-size, 64px);
451
+ height: var(--polycss-atlas-size, 64px);
398
452
  }
399
453
 
400
454
  .polycss-scene u {
401
- width: 0px;
402
- height: 0px;
455
+ width: 0;
456
+ height: 0;
403
457
  background: transparent;
404
458
  box-sizing: content-box;
405
459
  border: 0 solid transparent;
460
+ border-color: transparent transparent currentColor transparent;
461
+ border-width: 0 32px 64px 32px;
406
462
  }
407
463
 
408
464
  /* \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 */
@@ -415,17 +471,43 @@ var CORE_BASE_STYLES = `
415
471
  * up half-culled (you see only the side faces, not the caps), and the
416
472
  * arrow looks like a flat strip instead of a 3D bar.
417
473
  *
418
- * Transitions on border-color and background-color smooth the
419
- * idle \u2192 hover \u2192 drag alpha changes. Baked-mode arrows render their
420
- * color via inline border-color, dynamic-mode via background-color
421
- * (rgb-with-alpha CSS calc); transitioning both covers either path.
474
+ * Transitions on color, border-color, and background-color smooth the
475
+ * idle \u2192 hover \u2192 drag alpha changes across rect, border-shape, triangle,
476
+ * and atlas paths.
422
477
  */
423
478
  .polycss-transform-controls i,
424
479
  .polycss-transform-controls b,
425
480
  .polycss-transform-controls s,
426
481
  .polycss-transform-controls u {
427
482
  backface-visibility: visible;
428
- transition: border-color 150ms ease-out, background-color 150ms ease-out;
483
+ transition: color 150ms ease-out, border-color 150ms ease-out, background-color 150ms ease-out;
484
+ }
485
+
486
+ /*
487
+ * Rotate rings are rendered as a single square quad per ring, then masked
488
+ * to a donut via a radial-gradient. --ring-inner-ratio is set inline by
489
+ * <PolyTransformControls> (= innerR / outerR, where outerR is the edge of
490
+ * the quad mapped to 50%). Hit-testing also uses the donut shape \u2014 see
491
+ * the ring-aware path in TransformControls.tsx. Single DOM node per ring.
492
+ */
493
+ .polycss-transform-ring i,
494
+ .polycss-transform-ring b,
495
+ .polycss-transform-ring s,
496
+ .polycss-transform-ring u {
497
+ --ring-inner-r: calc(var(--ring-inner-ratio, 0.92) * 50%);
498
+ --ring-outer-r: calc(var(--ring-outer-ratio, 1) * 50%);
499
+ -webkit-mask: radial-gradient(circle at 50% 50%,
500
+ transparent 0%,
501
+ transparent var(--ring-inner-r),
502
+ black var(--ring-inner-r),
503
+ black var(--ring-outer-r),
504
+ transparent var(--ring-outer-r));
505
+ mask: radial-gradient(circle at 50% 50%,
506
+ transparent 0%,
507
+ transparent var(--ring-inner-r),
508
+ black var(--ring-inner-r),
509
+ black var(--ring-outer-r),
510
+ transparent var(--ring-outer-r));
429
511
  }
430
512
 
431
513
  /* \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 */
@@ -445,6 +527,20 @@ var CORE_BASE_STYLES = `
445
527
  @property --plx { syntax: "<number>"; inherits: true; initial-value: 0; }
446
528
  @property --ply { syntax: "<number>"; inherits: true; initial-value: 0; }
447
529
  @property --plz { syntax: "<number>"; inherits: true; initial-value: 1; }
530
+
531
+ /* CSS-space light components (world-Y\u2192cssX, world-X\u2192cssY, world-Z\u2192cssZ).
532
+ Used by the shadow projection matrix. --clz is clamped away from 0 in JS
533
+ to avoid divide-by-zero when the light is near-horizontal. */
534
+ @property --clx { syntax: "<number>"; inherits: true; initial-value: 0.01; }
535
+ @property --cly { syntax: "<number>"; inherits: true; initial-value: 0; }
536
+ @property --clz { syntax: "<number>"; inherits: true; initial-value: 1; }
537
+
538
+ /* Ground-plane position in CSS pixels along the CSS-Z axis (= world-Z, the
539
+ up axis in polycss's world convention). Stored as a <number> so it can be
540
+ used directly inside matrix3d() calc() expressions (matrix3d requires
541
+ dimensionless entries \u2014 no px units).
542
+ Set by PolyScene from the min world-Z of casting meshes. */
543
+ @property --shadow-ground-cssz { syntax: "<number>"; inherits: true; initial-value: 0; }
448
544
  @property --plr { syntax: "<number>"; inherits: true; initial-value: 1; }
449
545
  @property --plg { syntax: "<number>"; inherits: true; initial-value: 1; }
450
546
  @property --plb { syntax: "<number>"; inherits: true; initial-value: 1; }
@@ -454,14 +550,14 @@ var CORE_BASE_STYLES = `
454
550
  @property --pab { syntax: "<number>"; inherits: true; initial-value: 1; }
455
551
  @property --pai { syntax: "<number>"; inherits: true; initial-value: 0.4; }
456
552
 
457
- /* Per-polygon surface normal \u2014 set inline by the renderer. inherits:false
458
- because each leaf has its own normal (no cascade). */
553
+ /* Per-polygon surface normal \u2014 set inline by the renderer. Base RGB channels
554
+ may be hoisted to a mesh wrapper, so they inherit unless overridden inline. */
459
555
  @property --pnx { syntax: "<number>"; inherits: false; initial-value: 0; }
460
556
  @property --pny { syntax: "<number>"; inherits: false; initial-value: 0; }
461
557
  @property --pnz { syntax: "<number>"; inherits: false; initial-value: 1; }
462
- @property --psr { syntax: "<number>"; inherits: false; initial-value: 1; }
463
- @property --psg { syntax: "<number>"; inherits: false; initial-value: 1; }
464
- @property --psb { syntax: "<number>"; inherits: false; initial-value: 1; }
558
+ @property --psr { syntax: "<number>"; inherits: true; initial-value: 1; }
559
+ @property --psg { syntax: "<number>"; inherits: true; initial-value: 1; }
560
+ @property --psb { syntax: "<number>"; inherits: true; initial-value: 1; }
465
561
 
466
562
  /* Calc-driven Lambert + tint, scoped to dynamic-lighting scenes. Lives
467
563
  here (not inline per polygon) so each leaf only carries its tiny normal
@@ -487,8 +583,9 @@ var CORE_BASE_STYLES = `
487
583
  background-blend-mode: multiply;
488
584
  }
489
585
 
586
+ .polycss-scene[data-polycss-lighting="dynamic"] b,
490
587
  .polycss-scene[data-polycss-lighting="dynamic"] u {
491
- border-bottom-color: rgb(
588
+ color: rgb(
492
589
  calc(255 * var(--psr) * (var(--par) * var(--pai)
493
590
  + var(--plr) * var(--pli) * max(0,
494
591
  var(--pnx) * var(--plx) +
@@ -506,6 +603,92 @@ var CORE_BASE_STYLES = `
506
603
  var(--pnz) * var(--plz))))
507
604
  );
508
605
  }
606
+
607
+ /* <q> \u2014 dedicated shadow leaf. Same border-shape rendering trick as <i>
608
+ (border-color: currentColor fills the polygon outline) but with its
609
+ own tag so we don't have to thread :not(.polycss-shadow) exclusions
610
+ through every dynamic-mode color rule. backface-visibility must be
611
+ visible because the projection matrix is near-rank-deficient and the
612
+ resulting plane's normal can read as back-facing under some camera
613
+ angles; the leaf is intentionally always painted. Strip the UA's
614
+ default ::before/::after open-/close-quote so the element is just a
615
+ styled box. */
616
+ .polycss-scene q {
617
+ position: absolute;
618
+ display: block;
619
+ transform-origin: 0 0;
620
+ transform-style: preserve-3d;
621
+ margin: 0;
622
+ padding: 0;
623
+ font: inherit;
624
+ font-weight: normal;
625
+ font-style: normal;
626
+ line-height: 0;
627
+ text-decoration: none;
628
+ backface-visibility: visible;
629
+ border-color: currentColor;
630
+ pointer-events: none;
631
+ will-change: transform;
632
+ }
633
+ .polycss-scene q::before,
634
+ .polycss-scene q::after {
635
+ content: none;
636
+ }
637
+
638
+ /* \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 */
639
+
640
+ /*
641
+ * Shadow projection matrix. Projects any 3D point P onto the horizontal
642
+ * ground plane (cssZ \u2248 G) along the CSS-space light direction (--clx/y/z).
643
+ *
644
+ * In polycss's world convention world Z is up (red-green plane is the
645
+ * floor in the axes helper). After the world\u2192CSS swap (Y\u2194X), world Z stays
646
+ * as CSS Z, so the ground plane normal in CSS space is +cssZ.
647
+ *
648
+ * The strict projection formula would set m22=0 (output.z is a constant G,
649
+ * the polygon is exactly flat). But Chromium SKIPS rendering elements
650
+ * whose composed transform matrix is non-invertible (singular). m22=0
651
+ * makes the matrix singular, so the shadow paints nothing even though it
652
+ * has a valid layout box. The fix: collapse along z by a near-zero
653
+ * scale (Z_SQUASH = 0.01) instead of exactly zero \u2014 output.z is then
654
+ * approximately G with ~1% drift from the input, full-rank and renderable.
655
+ * The shadow still looks flat to the eye (the drift is sub-pixel for
656
+ * any realistic scene size).
657
+ *
658
+ * out.cssX = P.cssX - (--clx/--clz) * (P.cssZ - G)
659
+ * out.cssY = P.cssY - (--cly/--clz) * (P.cssZ - G)
660
+ * out.cssZ = Z_SQUASH * P.cssZ + (1 - Z_SQUASH) * G
661
+ *
662
+ * As column-major 4\xD74 (CSS matrix3d order):
663
+ * col1: [1, 0, 0, 0]
664
+ * col2: [0, 1, 0, 0]
665
+ * col3: [-(--clx/--clz), -(--cly/--clz), Z_SQUASH, 0]
666
+ * col4: [G*(--clx/--clz), G*(--cly/--clz), G*(1-Z_SQUASH), 1]
667
+ */
668
+ .polycss-scene[data-polycss-lighting="dynamic"] {
669
+ --shadow-proj: matrix3d(
670
+ 1, 0, 0, 0,
671
+ 0, 1, 0, 0,
672
+ calc(-1 * var(--clx) / var(--clz)),
673
+ calc(-1 * var(--cly) / var(--clz)),
674
+ 0.01,
675
+ 0,
676
+ calc(var(--shadow-ground-cssz) * var(--clx) / var(--clz)),
677
+ calc(var(--shadow-ground-cssz) * var(--cly) / var(--clz)),
678
+ calc(var(--shadow-ground-cssz) * 0.99),
679
+ 1
680
+ );
681
+ }
682
+
683
+ /* <q> shadow leaf \u2014 Lambert-gated opacity. Polygons facing the light cast
684
+ full shadow; polygons facing away cast zero shadow (their projection
685
+ would stack inside the silhouette and produce ugly overdraw). The
686
+ * 10 multiplier sharpens the cutoff so small positive Lambert values
687
+ jump quickly to 1, giving a near-binary visibility decision with a
688
+ smooth edge transition. Pure CSS calc \u2014 no JS at light-change time. */
689
+ .polycss-scene q {
690
+ opacity: clamp(0, calc((var(--pnx) * var(--clx) + var(--pny) * var(--cly) + var(--pnz) * var(--clz)) * 10), 1);
691
+ }
509
692
  `;
510
693
 
511
694
  // src/scene/textureAtlas.tsx
@@ -525,15 +708,30 @@ var MAX_ATLAS_SCALE = 1;
525
708
  var AUTO_ATLAS_LOW_AREA = ATLAS_MAX_SIZE * ATLAS_MAX_SIZE;
526
709
  var AUTO_ATLAS_MEDIUM_AREA = AUTO_ATLAS_LOW_AREA * 3;
527
710
  var AUTO_ATLAS_MAX_BITMAP_SIDE = 2048;
528
- var AUTO_ATLAS_MAX_DECODED_BYTES = 16 * 1024 * 1024;
711
+ var AUTO_ATLAS_MAX_DECODED_BYTES_MOBILE = 4 * 1024 * 1024;
712
+ var AUTO_ATLAS_MAX_DECODED_BYTES_DESKTOP = 16 * 1024 * 1024;
529
713
  var AUTO_ATLAS_SCALE_GUARD = 0.995;
530
714
  var DEFAULT_MATRIX_DECIMALS = 3;
531
715
  var DEFAULT_BORDER_SHAPE_DECIMALS = 2;
716
+ var DEFAULT_ATLAS_CSS_DECIMALS = 4;
717
+ var SOLID_QUAD_CANONICAL_SIZE = 64;
718
+ var SOLID_TRIANGLE_CANONICAL_SIZE = 64;
719
+ var ATLAS_CANONICAL_SIZE_EXPLICIT = 64;
720
+ var ATLAS_CANONICAL_SIZE_AUTO_DESKTOP = 128;
721
+ var BORDER_SHAPE_CENTER_PERCENT = 50;
722
+ var BORDER_SHAPE_POINT_EPS = 1e-7;
723
+ var BORDER_SHAPE_CANONICAL_SIZE = 16;
724
+ var PROJECTIVE_QUAD_DENOM_EPS = 0.05;
725
+ var PROJECTIVE_QUAD_MAX_WEIGHT_RATIO = 4;
726
+ var PROJECTIVE_QUAD_BLEED = 0.6;
532
727
  var BASIS_EPS = 1e-9;
533
- var SOLID_TRIANGLE_BLEED = 0.45;
728
+ var SOLID_TRIANGLE_BLEED = 0.6;
534
729
  var TEXTURE_IMAGE_CACHE = /* @__PURE__ */ new Map();
535
730
  var RECT_EPS = 1e-3;
536
731
  var TEXTURE_TRIANGLE_BLEED = 0.75;
732
+ var TEXTURE_EDGE_REPAIR_ALPHA_MIN = 1;
733
+ var TEXTURE_EDGE_REPAIR_SOURCE_ALPHA_MIN = 250;
734
+ var TEXTURE_EDGE_REPAIR_RADIUS = 1.5;
537
735
  function loadTextureImage(url) {
538
736
  let p = TEXTURE_IMAGE_CACHE.get(url);
539
737
  if (!p) {
@@ -565,6 +763,10 @@ function roundDecimal(value, decimals) {
565
763
  const next = value.toFixed(decimals).replace(/\.?0+$/, "");
566
764
  return Object.is(Number(next), -0) ? "0" : next;
567
765
  }
766
+ function formatCssLength(value, decimals = DEFAULT_ATLAS_CSS_DECIMALS) {
767
+ const next = roundDecimal(value, decimals);
768
+ return Number(next) === 0 || Object.is(Number(next), -0) ? "0" : `${next}px`;
769
+ }
568
770
  function formatMatrix3d(matrix, decimals = DEFAULT_MATRIX_DECIMALS) {
569
771
  return `matrix3d(${matrix.split(",").map((value) => {
570
772
  const parsed = Number(value.trim());
@@ -572,12 +774,31 @@ function formatMatrix3d(matrix, decimals = DEFAULT_MATRIX_DECIMALS) {
572
774
  }).join(",")})`;
573
775
  }
574
776
  function formatPercent(value, decimals = DEFAULT_BORDER_SHAPE_DECIMALS) {
575
- return `${roundDecimal(value, decimals)}%`;
777
+ const next = roundDecimal(value, decimals);
778
+ return Number(next) === 0 ? "0" : `${next}%`;
779
+ }
780
+ function pointOnSegment(px, py, ax, ay, bx, by) {
781
+ const cross = (px - ax) * (by - ay) - (py - ay) * (bx - ax);
782
+ if (Math.abs(cross) > BORDER_SHAPE_POINT_EPS) return false;
783
+ const dot = (px - ax) * (px - bx) + (py - ay) * (py - by);
784
+ return dot <= BORDER_SHAPE_POINT_EPS;
785
+ }
786
+ function polygonContainsPoint(points, px = BORDER_SHAPE_CENTER_PERCENT, py = BORDER_SHAPE_CENTER_PERCENT) {
787
+ let inside = false;
788
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
789
+ const [xi, yi] = points[i];
790
+ const [xj, yj] = points[j];
791
+ if (pointOnSegment(px, py, xi, yi, xj, yj)) return true;
792
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
793
+ inside = !inside;
794
+ }
795
+ }
796
+ return inside;
576
797
  }
577
798
  function atlasArea(pages) {
578
799
  return pages.reduce((sum, page) => sum + page.width * page.height, 0);
579
800
  }
580
- function autoAtlasScaleCap(pages) {
801
+ function autoAtlasScaleCap(pages, maxDecodedBytes) {
581
802
  const area = atlasArea(pages);
582
803
  if (area <= 0) return 1;
583
804
  const maxSide = Math.max(
@@ -585,15 +806,52 @@ function autoAtlasScaleCap(pages) {
585
806
  ...pages.map((page) => Math.max(page.width, page.height))
586
807
  );
587
808
  const sideScale = AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide;
588
- const memoryScale = Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / (area * 4));
809
+ const memoryScale = Math.sqrt(maxDecodedBytes / (area * 4));
589
810
  return normalizeAtlasScale(Math.min(sideScale, memoryScale));
590
811
  }
591
- function autoAtlasScale(pages) {
812
+ function isMobileDocument(doc) {
813
+ if (!doc) return false;
814
+ const win = doc.defaultView ?? (typeof window !== "undefined" ? window : void 0);
815
+ const media = win?.matchMedia;
816
+ if (!media) return false;
817
+ return media("(pointer: coarse)").matches || media("(hover: none)").matches;
818
+ }
819
+ function autoAtlasMaxDecodedBytes(doc) {
820
+ return isMobileDocument(doc) ? AUTO_ATLAS_MAX_DECODED_BYTES_MOBILE : AUTO_ATLAS_MAX_DECODED_BYTES_DESKTOP;
821
+ }
822
+ function atlasCanonicalSizeForTextureQuality(textureQualityInput, doc) {
823
+ if (textureQualityInput !== void 0 && textureQualityInput !== "auto") {
824
+ return ATLAS_CANONICAL_SIZE_EXPLICIT;
825
+ }
826
+ return isMobileDocument(doc) ? ATLAS_CANONICAL_SIZE_EXPLICIT : ATLAS_CANONICAL_SIZE_AUTO_DESKTOP;
827
+ }
828
+ function formatAtlasMatrix(entry, atlasCanonicalSize) {
829
+ const values = entry.matrix.split(",").map((value) => Number(value));
830
+ if (values.length !== 16 || values.some((value) => !Number.isFinite(value))) {
831
+ return entry.canonicalMatrix;
832
+ }
833
+ values[0] *= entry.canvasW / atlasCanonicalSize;
834
+ values[1] *= entry.canvasW / atlasCanonicalSize;
835
+ values[2] *= entry.canvasW / atlasCanonicalSize;
836
+ values[4] *= entry.canvasH / atlasCanonicalSize;
837
+ values[5] *= entry.canvasH / atlasCanonicalSize;
838
+ values[6] *= entry.canvasH / atlasCanonicalSize;
839
+ return values.join(",");
840
+ }
841
+ function applyPackedAtlasCanonicalSize(packed, atlasCanonicalSize) {
842
+ for (const entry of packed.entries) {
843
+ if (!entry) continue;
844
+ entry.atlasCanonicalSize = atlasCanonicalSize;
845
+ entry.atlasMatrix = formatAtlasMatrix(entry, atlasCanonicalSize);
846
+ }
847
+ return packed;
848
+ }
849
+ function autoAtlasScale(pages, maxDecodedBytes) {
592
850
  const area = atlasArea(pages);
593
851
  let atlasScale = 0.5;
594
852
  if (area <= AUTO_ATLAS_LOW_AREA) atlasScale = 1;
595
853
  else if (area <= AUTO_ATLAS_MEDIUM_AREA) atlasScale = 0.75;
596
- return normalizeAtlasScale(Math.min(atlasScale, autoAtlasScaleCap(pages)));
854
+ return normalizeAtlasScale(Math.min(atlasScale, autoAtlasScaleCap(pages, maxDecodedBytes)));
597
855
  }
598
856
  function atlasBitmapMaxSide(pages, atlasScale) {
599
857
  return pages.reduce((max, page) => Math.max(
@@ -608,18 +866,18 @@ function atlasDecodedBytes(pages, atlasScale) {
608
866
  0
609
867
  );
610
868
  }
611
- function autoAtlasBudgetFactor(pages, atlasScale) {
869
+ function autoAtlasBudgetFactor(pages, atlasScale, maxDecodedBytes) {
612
870
  const maxSide = atlasBitmapMaxSide(pages, atlasScale);
613
871
  const decodedBytes = atlasDecodedBytes(pages, atlasScale);
614
872
  const sideFactor = maxSide > AUTO_ATLAS_MAX_BITMAP_SIDE ? AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide : 1;
615
- const memoryFactor = decodedBytes > AUTO_ATLAS_MAX_DECODED_BYTES ? Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / decodedBytes) : 1;
873
+ const memoryFactor = decodedBytes > maxDecodedBytes ? Math.sqrt(maxDecodedBytes / decodedBytes) : 1;
616
874
  return Math.min(sideFactor, memoryFactor);
617
875
  }
618
- function packTextureAtlasPlansAuto(plans, fullScalePacked) {
619
- let atlasScale = autoAtlasScale(fullScalePacked.pages);
876
+ function packTextureAtlasPlansAuto(plans, fullScalePacked, maxDecodedBytes) {
877
+ let atlasScale = autoAtlasScale(fullScalePacked.pages, maxDecodedBytes);
620
878
  let packed = atlasScale === 1 ? fullScalePacked : packTextureAtlasPlans(plans, atlasScale);
621
879
  for (let i = 0; i < 4; i++) {
622
- const factor = autoAtlasBudgetFactor(packed.pages, atlasScale);
880
+ const factor = autoAtlasBudgetFactor(packed.pages, atlasScale, maxDecodedBytes);
623
881
  if (factor >= 1) break;
624
882
  const nextAtlasScale = normalizeAtlasScale(atlasScale * factor * AUTO_ATLAS_SCALE_GUARD);
625
883
  if (nextAtlasScale >= atlasScale) break;
@@ -628,13 +886,23 @@ function packTextureAtlasPlansAuto(plans, fullScalePacked) {
628
886
  }
629
887
  return { packed, atlasScale };
630
888
  }
631
- function packTextureAtlasPlansWithScale(plans, atlasScaleInput) {
632
- if (atlasScaleInput !== void 0 && atlasScaleInput !== "auto") {
633
- const atlasScale = normalizeAtlasScale(atlasScaleInput);
634
- return { packed: packTextureAtlasPlans(plans, atlasScale), atlasScale };
889
+ function packTextureAtlasPlansWithScale(plans, textureQualityInput, doc) {
890
+ const atlasCanonicalSize = atlasCanonicalSizeForTextureQuality(textureQualityInput, doc);
891
+ if (textureQualityInput !== void 0 && textureQualityInput !== "auto") {
892
+ const atlasScale = normalizeAtlasScale(textureQualityInput);
893
+ return {
894
+ packed: applyPackedAtlasCanonicalSize(packTextureAtlasPlans(plans, atlasScale), atlasCanonicalSize),
895
+ atlasScale,
896
+ atlasCanonicalSize
897
+ };
635
898
  }
636
899
  const fullScalePacked = packTextureAtlasPlans(plans, 1);
637
- return packTextureAtlasPlansAuto(plans, fullScalePacked);
900
+ const autoPacked = packTextureAtlasPlansAuto(plans, fullScalePacked, autoAtlasMaxDecodedBytes(doc));
901
+ return {
902
+ packed: applyPackedAtlasCanonicalSize(autoPacked.packed, atlasCanonicalSize),
903
+ atlasScale: autoPacked.atlasScale,
904
+ atlasCanonicalSize
905
+ };
638
906
  }
639
907
  function atlasPadding(atlasScale) {
640
908
  return Math.max(ATLAS_PADDING, Math.ceil(ATLAS_PADDING / atlasScale));
@@ -654,6 +922,9 @@ function parseHex(hex) {
654
922
  if (!parsed) return { r: 255, g: 255, b: 255 };
655
923
  return { r: parsed.rgb[0], g: parsed.rgb[1], b: parsed.rgb[2] };
656
924
  }
925
+ function rgbKey({ r, g, b }) {
926
+ return `${r},${g},${b}`;
927
+ }
657
928
  function parseAlpha(input) {
658
929
  return (0, import_polycss_core3.parsePureColor)(input)?.alpha ?? 1;
659
930
  }
@@ -689,60 +960,369 @@ function isFullRectSolid(entry) {
689
960
  function isSolidTrianglePlan(entry) {
690
961
  return !entry.texture && entry.polygon.vertices.length === 3;
691
962
  }
963
+ function isProjectiveQuadPlan(entry) {
964
+ return !entry.texture && !!entry.projectiveMatrix && !isFullRectSolid(entry);
965
+ }
692
966
  function borderShapeSupported() {
693
967
  const supportsBorderShape = !!globalThis.CSS?.supports?.(
694
968
  "border-shape",
695
- "polygon(0 0, 100% 0, 0 100%) polygon(50% 50%, 50% 50%, 50% 50%)"
969
+ "polygon(0 0, 100% 0, 0 100%) circle(0)"
696
970
  );
697
971
  if (!supportsBorderShape) return false;
698
972
  const media = globalThis.matchMedia;
699
973
  if (typeof media !== "function") return true;
700
974
  return media("(pointer: fine)").matches && media("(hover: hover)").matches;
701
975
  }
702
- function cssPolygonShapeForPlan(entry) {
703
- const pts = [];
976
+ function solidTriangleSupported() {
977
+ const userAgent = (typeof window !== "undefined" ? window.navigator : globalThis.navigator)?.userAgent ?? "";
978
+ if (!userAgent) return true;
979
+ const isChromiumFamily = /\b(?:Chrome|HeadlessChrome|Chromium|Edg|OPR)\//.test(userAgent);
980
+ const isSafariFamily = /\bVersion\/[\d.]+.*\bSafari\//.test(userAgent);
981
+ return !isSafariFamily || isChromiumFamily;
982
+ }
983
+ function incrementCount(map, key) {
984
+ map.set(key, (map.get(key) ?? 0) + 1);
985
+ }
986
+ function dominantCountKey(map) {
987
+ let bestKey;
988
+ let bestCount = 1;
989
+ for (const [key, count] of map) {
990
+ if (count > bestCount) {
991
+ bestKey = key;
992
+ bestCount = count;
993
+ }
994
+ }
995
+ return bestKey;
996
+ }
997
+ var BRUSH_INLINE_STYLE_ORDER = /* @__PURE__ */ new Map([
998
+ ["transform", 0],
999
+ ["border-shape", 1],
1000
+ ["border-width", 2],
1001
+ ["width", 3],
1002
+ ["height", 4],
1003
+ ["color", 5]
1004
+ ]);
1005
+ function orderBrushInlineStyle(el) {
1006
+ const current = el.getAttribute("style");
1007
+ if (!current) return;
1008
+ const declarations = current.split(";").map((declaration) => declaration.trim()).filter(Boolean);
1009
+ const next = declarations.map((declaration, index) => {
1010
+ const property = declaration.slice(0, declaration.indexOf(":")).trim().toLowerCase();
1011
+ return {
1012
+ declaration,
1013
+ index,
1014
+ order: BRUSH_INLINE_STYLE_ORDER.get(property) ?? Number.POSITIVE_INFINITY
1015
+ };
1016
+ }).sort((a, b) => a.order - b.order || a.index - b.index).map(({ declaration }) => declaration).join(";");
1017
+ if (next !== current) el.setAttribute("style", next);
1018
+ }
1019
+ function getSolidPaintDefaults(plans, textureLighting) {
1020
+ const paintCounts = /* @__PURE__ */ new Map();
1021
+ const dynamicCounts = /* @__PURE__ */ new Map();
1022
+ const dynamicColors = /* @__PURE__ */ new Map();
1023
+ const useStableTriangle = solidTriangleSupported();
1024
+ const useBorderShape = textureLighting !== "dynamic" && borderShapeSupported();
1025
+ for (const plan of plans) {
1026
+ if (!plan || plan.texture) continue;
1027
+ if (textureLighting === "dynamic") {
1028
+ if (!(useStableTriangle && isSolidTrianglePlan(plan)) && !isFullRectSolid(plan)) continue;
1029
+ const color = parseHex(plan.polygon.color ?? "#cccccc");
1030
+ const key = rgbKey(color);
1031
+ incrementCount(dynamicCounts, key);
1032
+ if (!dynamicColors.has(key)) dynamicColors.set(key, color);
1033
+ continue;
1034
+ }
1035
+ if (!(useStableTriangle && isSolidTrianglePlan(plan)) && !isFullRectSolid(plan) && !useBorderShape) continue;
1036
+ incrementCount(paintCounts, plan.shadedColor);
1037
+ }
1038
+ const paintColor = dominantCountKey(paintCounts);
1039
+ const dynamicColorKey = dominantCountKey(dynamicCounts);
1040
+ return {
1041
+ paintColor,
1042
+ dynamicColorKey,
1043
+ dynamicColor: dynamicColorKey ? dynamicColors.get(dynamicColorKey) : void 0
1044
+ };
1045
+ }
1046
+ function borderShapePointsForPlan(entry) {
1047
+ const points = [];
704
1048
  const width = entry.canvasW || 1;
705
1049
  const height = entry.canvasH || 1;
706
1050
  for (let i = 0; i < entry.screenPts.length; i += 2) {
707
1051
  const x = Math.max(0, Math.min(100, entry.screenPts[i] / width * 100));
708
1052
  const y = Math.max(0, Math.min(100, entry.screenPts[i + 1] / height * 100));
709
- pts.push(`${formatPercent(x)} ${formatPercent(y)}`);
1053
+ points.push([x, y]);
710
1054
  }
711
- return `polygon(${pts.join(", ")})`;
1055
+ return points;
1056
+ }
1057
+ function cssBorderShapePoint([x, y]) {
1058
+ return `${formatPercent(x)} ${formatPercent(y)}`;
1059
+ }
1060
+ function cssPolygonShapeForPoints(points) {
1061
+ return `polygon(${points.map(cssBorderShapePoint).join(",")})`;
712
1062
  }
713
- function cssCollapsedInnerShapeForPlan(entry) {
1063
+ function cssCollapsedInnerShapeForPoints(points) {
1064
+ if (polygonContainsPoint(points)) return "circle(0)";
714
1065
  let xSum = 0;
715
1066
  let ySum = 0;
716
- const points = Math.max(1, entry.screenPts.length / 2);
717
- for (let i = 0; i < entry.screenPts.length; i += 2) {
718
- xSum += entry.screenPts[i];
719
- ySum += entry.screenPts[i + 1];
1067
+ const pointCount = Math.max(1, points.length);
1068
+ for (const [x2, y2] of points) {
1069
+ xSum += x2;
1070
+ ySum += y2;
720
1071
  }
721
- const width = entry.canvasW || 1;
722
- const height = entry.canvasH || 1;
723
- const x = formatPercent(Math.max(0, Math.min(100, xSum / points / width * 100)));
724
- const y = formatPercent(Math.max(0, Math.min(100, ySum / points / height * 100)));
725
- return `polygon(${Array.from({ length: points }, () => `${x} ${y}`).join(", ")})`;
1072
+ const x = formatPercent(Math.max(0, Math.min(100, xSum / pointCount)));
1073
+ const y = formatPercent(Math.max(0, Math.min(100, ySum / pointCount)));
1074
+ return `circle(0 at ${x} ${y})`;
726
1075
  }
727
1076
  function cssBorderShapeForPlan(entry) {
728
- return `${cssPolygonShapeForPlan(entry)} ${cssCollapsedInnerShapeForPlan(entry)}`;
1077
+ const points = borderShapePointsForPlan(entry);
1078
+ return `${cssPolygonShapeForPoints(points)} ${cssCollapsedInnerShapeForPoints(points)}`;
729
1079
  }
730
1080
  function formatMatrix3dValues(values, decimals = DEFAULT_MATRIX_DECIMALS) {
731
1081
  return values.map((value) => roundDecimal(value, decimals)).join(",");
732
1082
  }
1083
+ function formatScaledMatrixFromPlan(entry, scaleX, scaleY) {
1084
+ const values = entry.matrix.split(",").map((value) => Number(value));
1085
+ if (values.length !== 16 || values.some((value) => !Number.isFinite(value))) {
1086
+ return entry.matrix;
1087
+ }
1088
+ values[0] *= scaleX;
1089
+ values[1] *= scaleX;
1090
+ values[2] *= scaleX;
1091
+ values[4] *= scaleY;
1092
+ values[5] *= scaleY;
1093
+ values[6] *= scaleY;
1094
+ return formatMatrix3dValues(values);
1095
+ }
1096
+ function formatBorderShapeMatrix(entry) {
1097
+ return formatScaledMatrixFromPlan(
1098
+ entry,
1099
+ (entry.canvasW || 1) / BORDER_SHAPE_CANONICAL_SIZE,
1100
+ (entry.canvasH || 1) / BORDER_SHAPE_CANONICAL_SIZE
1101
+ );
1102
+ }
1103
+ function formatSolidQuadMatrix(entry) {
1104
+ return formatScaledMatrixFromPlan(
1105
+ entry,
1106
+ (entry.canvasW || 1) / SOLID_QUAD_CANONICAL_SIZE,
1107
+ (entry.canvasH || 1) / SOLID_QUAD_CANONICAL_SIZE
1108
+ );
1109
+ }
1110
+ function isConvexPolygonPoints(points) {
1111
+ if (points.length < 3) return false;
1112
+ let sign = 0;
1113
+ for (let i = 0; i < points.length; i++) {
1114
+ const a = points[i];
1115
+ const b = points[(i + 1) % points.length];
1116
+ const c = points[(i + 2) % points.length];
1117
+ const cross = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
1118
+ if (Math.abs(cross) <= BASIS_EPS) return false;
1119
+ const nextSign = Math.sign(cross);
1120
+ if (sign === 0) sign = nextSign;
1121
+ else if (nextSign !== sign) return false;
1122
+ }
1123
+ return true;
1124
+ }
1125
+ function signedArea2D(points) {
1126
+ let area = 0;
1127
+ for (let i = 0; i < points.length; i++) {
1128
+ const a = points[i];
1129
+ const b = points[(i + 1) % points.length];
1130
+ area += a[0] * b[1] - a[1] * b[0];
1131
+ }
1132
+ return area / 2;
1133
+ }
1134
+ function intersect2DLines(a0, a1, b0, b1) {
1135
+ const rx = a1[0] - a0[0];
1136
+ const ry = a1[1] - a0[1];
1137
+ const sx = b1[0] - b0[0];
1138
+ const sy = b1[1] - b0[1];
1139
+ const det = rx * sy - ry * sx;
1140
+ if (Math.abs(det) <= BASIS_EPS) return null;
1141
+ const qpx = b0[0] - a0[0];
1142
+ const qpy = b0[1] - a0[1];
1143
+ const t = (qpx * sy - qpy * sx) / det;
1144
+ return [a0[0] + t * rx, a0[1] + t * ry];
1145
+ }
1146
+ function offsetConvexPolygonPoints(points, amount) {
1147
+ if (points.length < 6 || points.length % 2 !== 0 || amount <= 0) return points;
1148
+ const q = [];
1149
+ for (let i = 0; i < points.length; i += 2) q.push([points[i], points[i + 1]]);
1150
+ if (!isConvexPolygonPoints(q)) return expandClipPoints(points, amount);
1151
+ const area = signedArea2D(q);
1152
+ if (Math.abs(area) <= BASIS_EPS) return expandClipPoints(points, amount);
1153
+ const outwardSign = area > 0 ? 1 : -1;
1154
+ const offsetLines = [];
1155
+ for (let i = 0; i < q.length; i++) {
1156
+ const a = q[i];
1157
+ const b = q[(i + 1) % q.length];
1158
+ const dx = b[0] - a[0];
1159
+ const dy = b[1] - a[1];
1160
+ const length = Math.hypot(dx, dy);
1161
+ if (length <= BASIS_EPS) return expandClipPoints(points, amount);
1162
+ const ox = outwardSign * (dy / length) * amount;
1163
+ const oy = outwardSign * (-dx / length) * amount;
1164
+ offsetLines.push({
1165
+ a: [a[0] + ox, a[1] + oy],
1166
+ b: [b[0] + ox, b[1] + oy]
1167
+ });
1168
+ }
1169
+ const expanded = [];
1170
+ const maxMiter = Math.max(2, amount * 4);
1171
+ for (let i = 0; i < q.length; i++) {
1172
+ const prev = offsetLines[(i + q.length - 1) % q.length];
1173
+ const next = offsetLines[i];
1174
+ const intersection = intersect2DLines(prev.a, prev.b, next.a, next.b);
1175
+ if (!intersection) return expandClipPoints(points, amount);
1176
+ const original = q[i];
1177
+ const dx = intersection[0] - original[0];
1178
+ const dy = intersection[1] - original[1];
1179
+ const miter = Math.hypot(dx, dy);
1180
+ if (miter > maxMiter) {
1181
+ expanded.push(
1182
+ original[0] + dx / miter * maxMiter,
1183
+ original[1] + dy / miter * maxMiter
1184
+ );
1185
+ } else {
1186
+ expanded.push(intersection[0], intersection[1]);
1187
+ }
1188
+ }
1189
+ return expanded;
1190
+ }
1191
+ function computeProjectiveQuadCoefficients(q) {
1192
+ if (q.length !== 4 || !isConvexPolygonPoints(q)) return null;
1193
+ const [q0, q1, q2, q3] = q;
1194
+ const sx = q0[0] - q1[0] + q2[0] - q3[0];
1195
+ const sy = q0[1] - q1[1] + q2[1] - q3[1];
1196
+ const dx1 = q1[0] - q2[0];
1197
+ const dx2 = q3[0] - q2[0];
1198
+ const dy1 = q1[1] - q2[1];
1199
+ const dy2 = q3[1] - q2[1];
1200
+ const det = dx1 * dy2 - dy1 * dx2;
1201
+ if (Math.abs(det) <= BASIS_EPS) return null;
1202
+ const g = (sx * dy2 - sy * dx2) / det;
1203
+ const h = (dx1 * sy - dy1 * sx) / det;
1204
+ const weights = [1, 1 + g, 1 + g + h, 1 + h];
1205
+ if (weights.some((weight) => !Number.isFinite(weight) || weight <= PROJECTIVE_QUAD_DENOM_EPS)) {
1206
+ return null;
1207
+ }
1208
+ const minWeight = Math.min(...weights);
1209
+ const maxWeight = Math.max(...weights);
1210
+ if (maxWeight / minWeight > PROJECTIVE_QUAD_MAX_WEIGHT_RATIO) return null;
1211
+ return {
1212
+ g,
1213
+ h,
1214
+ w1: 1 + g,
1215
+ w3: 1 + h
1216
+ };
1217
+ }
1218
+ function computeProjectiveQuadMatrix(screenPts, xAxis, yAxis, normal, tx, ty, tz) {
1219
+ if (screenPts.length !== 8) return null;
1220
+ const rawQ = [
1221
+ [screenPts[0], screenPts[1]],
1222
+ [screenPts[2], screenPts[3]],
1223
+ [screenPts[4], screenPts[5]],
1224
+ [screenPts[6], screenPts[7]]
1225
+ ];
1226
+ if (!computeProjectiveQuadCoefficients(rawQ)) return null;
1227
+ const expandedPts = offsetConvexPolygonPoints(screenPts, PROJECTIVE_QUAD_BLEED);
1228
+ const q = [
1229
+ [expandedPts[0], expandedPts[1]],
1230
+ [expandedPts[2], expandedPts[3]],
1231
+ [expandedPts[4], expandedPts[5]],
1232
+ [expandedPts[6], expandedPts[7]]
1233
+ ];
1234
+ const coeffs = computeProjectiveQuadCoefficients(q);
1235
+ if (!coeffs) return null;
1236
+ const { g, h, w1, w3 } = coeffs;
1237
+ const [q0, q1, , q3] = q;
1238
+ const toCssPoint = ([x, y]) => [
1239
+ tx + x * xAxis[0] + y * yAxis[0],
1240
+ ty + x * xAxis[1] + y * yAxis[1],
1241
+ tz + x * xAxis[2] + y * yAxis[2]
1242
+ ];
1243
+ const p0 = toCssPoint(q0);
1244
+ const p1 = toCssPoint(q1);
1245
+ const p3 = toCssPoint(q3);
1246
+ const xCol = [
1247
+ p1[0] * w1 - p0[0],
1248
+ p1[1] * w1 - p0[1],
1249
+ p1[2] * w1 - p0[2]
1250
+ ];
1251
+ const yCol = [
1252
+ p3[0] * w3 - p0[0],
1253
+ p3[1] * w3 - p0[1],
1254
+ p3[2] * w3 - p0[2]
1255
+ ];
1256
+ const values = [
1257
+ xCol[0],
1258
+ xCol[1],
1259
+ xCol[2],
1260
+ g,
1261
+ yCol[0],
1262
+ yCol[1],
1263
+ yCol[2],
1264
+ h,
1265
+ normal[0],
1266
+ normal[1],
1267
+ normal[2],
1268
+ 0,
1269
+ p0[0],
1270
+ p0[1],
1271
+ p0[2],
1272
+ 1
1273
+ ];
1274
+ for (let i = 0; i < 8; i += 1) values[i] /= SOLID_QUAD_CANONICAL_SIZE;
1275
+ return values.join(",");
1276
+ }
733
1277
  function cssPoints(vertices, tile, elev) {
734
1278
  return vertices.map((v) => [v[1] * tile, v[0] * tile, v[2] * elev]);
735
1279
  }
1280
+ function pointKey(point) {
1281
+ return `${point[0]},${point[1]},${point[2]}`;
1282
+ }
1283
+ function edgeKey(a, b) {
1284
+ const ak = pointKey(a);
1285
+ const bk = pointKey(b);
1286
+ return ak < bk ? `${ak}|${bk}` : `${bk}|${ak}`;
1287
+ }
1288
+ function buildTextureEdgeRepairSets(polygons) {
1289
+ const edgeOwners = /* @__PURE__ */ new Map();
1290
+ for (let polygonIndex = 0; polygonIndex < polygons.length; polygonIndex++) {
1291
+ const vertices = polygons[polygonIndex].vertices;
1292
+ if (!vertices || vertices.length < 3 || !polygons[polygonIndex].texture) continue;
1293
+ for (let edgeIndex = 0; edgeIndex < vertices.length; edgeIndex++) {
1294
+ const key = edgeKey(vertices[edgeIndex], vertices[(edgeIndex + 1) % vertices.length]);
1295
+ const owner = { polygon: polygonIndex, edge: edgeIndex };
1296
+ const owners = edgeOwners.get(key);
1297
+ if (owners) owners.push(owner);
1298
+ else edgeOwners.set(key, [owner]);
1299
+ }
1300
+ }
1301
+ const repairEdges = polygons.map(() => /* @__PURE__ */ new Set());
1302
+ for (const owners of edgeOwners.values()) {
1303
+ if (owners.length < 2) continue;
1304
+ for (let i = 0; i < owners.length; i++) {
1305
+ for (let j = i + 1; j < owners.length; j++) {
1306
+ repairEdges[owners[i].polygon].add(owners[i].edge);
1307
+ repairEdges[owners[j].polygon].add(owners[j].edge);
1308
+ }
1309
+ }
1310
+ }
1311
+ return repairEdges.map((edges) => edges.size > 0 ? edges : void 0);
1312
+ }
736
1313
  function computeSurfaceNormal(pts) {
737
1314
  if (pts.length < 3) return null;
738
- const p0 = pts[0], p1 = pts[1], p2 = pts[2];
739
- const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
740
- const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
741
- const normal = [
742
- -(e1[1] * e2[2] - e1[2] * e2[1]),
743
- -(e1[2] * e2[0] - e1[0] * e2[2]),
744
- -(e1[0] * e2[1] - e1[1] * e2[0])
745
- ];
1315
+ const p0 = pts[0];
1316
+ const normal = [0, 0, 0];
1317
+ for (let i = 1; i + 1 < pts.length; i++) {
1318
+ const p1 = pts[i];
1319
+ const p2 = pts[i + 1];
1320
+ const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
1321
+ const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
1322
+ normal[0] -= e1[1] * e2[2] - e1[2] * e2[1];
1323
+ normal[1] -= e1[2] * e2[0] - e1[0] * e2[2];
1324
+ normal[2] -= e1[0] * e2[1] - e1[1] * e2[0];
1325
+ }
746
1326
  const len = Math.hypot(normal[0], normal[1], normal[2]);
747
1327
  if (len <= BASIS_EPS) return null;
748
1328
  return [normal[0] / len, normal[1] / len, normal[2] / len];
@@ -757,7 +1337,7 @@ function crossVec(a, b) {
757
1337
  a[0] * b[1] - a[1] * b[0]
758
1338
  ];
759
1339
  }
760
- function solidTriangleStyle(entry, textureLighting, pointerEvents) {
1340
+ function solidTriangleStyle(entry, textureLighting, pointerEvents, solidPaintDefaults) {
761
1341
  if (!isSolidTrianglePlan(entry)) return null;
762
1342
  const pts = cssPoints(entry.polygon.vertices, entry.tileSize, entry.layerElevation);
763
1343
  const normal = computeSurfaceNormal(pts);
@@ -841,47 +1421,89 @@ function solidTriangleStyle(entry, textureLighting, pointerEvents) {
841
1421
  }
842
1422
  const left = Math.max(0, Math.min(baseLength, apexX));
843
1423
  const right = Math.max(0, baseLength - left);
844
- const bleed = SOLID_TRIANGLE_BLEED;
845
- const leftPx = left + bleed;
846
- const rightPx = right + bleed;
847
- const heightPx = height + bleed * 2;
848
- const tx = cv[0] - leftPx * xAxis[0] - bleed * yAxis[0];
849
- const ty = cv[1] - leftPx * xAxis[1] - bleed * yAxis[1];
850
- const tz = cv[2] - leftPx * xAxis[2] - bleed * yAxis[2];
851
- const matrix = formatMatrix3dValues([
852
- xAxis[0],
853
- xAxis[1],
854
- xAxis[2],
855
- 0,
856
- yAxis[0],
857
- yAxis[1],
858
- yAxis[2],
1424
+ const expanded = offsetConvexPolygonPoints([
1425
+ left,
859
1426
  0,
860
- normal[0],
861
- normal[1],
862
- normal[2],
863
1427
  0,
864
- tx,
865
- ty,
866
- tz,
867
- 1
868
- ]);
1428
+ height,
1429
+ left + right,
1430
+ height
1431
+ ], SOLID_TRIANGLE_BLEED);
1432
+ const apex2 = [expanded[0], expanded[1]];
1433
+ const baseLeft2 = [expanded[2], expanded[3]];
1434
+ const baseRight2 = [expanded[4], expanded[5]];
1435
+ const baseY = (baseLeft2[1] + baseRight2[1]) / 2;
1436
+ const leftPx = apex2[0] - baseLeft2[0];
1437
+ const rightPx = baseRight2[0] - apex2[0];
1438
+ const heightPx = baseY - apex2[1];
1439
+ if (leftPx <= BASIS_EPS || rightPx <= BASIS_EPS || heightPx <= BASIS_EPS || !Number.isFinite(leftPx + rightPx + heightPx)) {
1440
+ return null;
1441
+ }
869
1442
  const dynamic = textureLighting === "dynamic";
870
1443
  const base = parseHex(entry.polygon.color ?? "#cccccc");
871
- return {
872
- transform: `matrix3d(${matrix})`,
873
- borderWidth: `0 ${rightPx}px ${heightPx}px ${leftPx}px`,
874
- borderBottomColor: dynamic ? void 0 : entry.shadedColor,
1444
+ const useDefaultDynamicColor = dynamic && rgbKey(base) === solidPaintDefaults?.dynamicColorKey;
1445
+ const sharedStyle = {
1446
+ color: dynamic || entry.shadedColor === solidPaintDefaults?.paintColor ? void 0 : entry.shadedColor,
875
1447
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
876
- ...dynamic ? {
1448
+ ...dynamic && !useDefaultDynamicColor ? {
877
1449
  ["--pnx"]: normal[0].toFixed(4),
878
1450
  ["--pny"]: normal[1].toFixed(4),
879
1451
  ["--pnz"]: normal[2].toFixed(4),
880
1452
  ["--psr"]: (base.r / 255).toFixed(4),
881
1453
  ["--psg"]: (base.g / 255).toFixed(4),
882
1454
  ["--psb"]: (base.b / 255).toFixed(4)
1455
+ } : dynamic ? {
1456
+ ["--pnx"]: normal[0].toFixed(4),
1457
+ ["--pny"]: normal[1].toFixed(4),
1458
+ ["--pnz"]: normal[2].toFixed(4)
883
1459
  } : null
884
1460
  };
1461
+ const worldPoint = ([x, y]) => [
1462
+ cv[0] + (x - left) * xAxis[0] + y * yAxis[0],
1463
+ cv[1] + (x - left) * xAxis[1] + y * yAxis[1],
1464
+ cv[2] + (x - left) * xAxis[2] + y * yAxis[2]
1465
+ ];
1466
+ const apex = worldPoint(apex2);
1467
+ const baseLeft = worldPoint([baseLeft2[0], baseY]);
1468
+ const baseRight = worldPoint([baseRight2[0], baseY]);
1469
+ const halfBase = SOLID_TRIANGLE_CANONICAL_SIZE / 2;
1470
+ const xCol = [
1471
+ (baseRight[0] - baseLeft[0]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1472
+ (baseRight[1] - baseLeft[1]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1473
+ (baseRight[2] - baseLeft[2]) / SOLID_TRIANGLE_CANONICAL_SIZE
1474
+ ];
1475
+ const txCol = [
1476
+ apex[0] - xCol[0] * halfBase,
1477
+ apex[1] - xCol[1] * halfBase,
1478
+ apex[2] - xCol[2] * halfBase
1479
+ ];
1480
+ const yCol = [
1481
+ (baseLeft[0] - txCol[0]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1482
+ (baseLeft[1] - txCol[1]) / SOLID_TRIANGLE_CANONICAL_SIZE,
1483
+ (baseLeft[2] - txCol[2]) / SOLID_TRIANGLE_CANONICAL_SIZE
1484
+ ];
1485
+ const canonicalMatrix = formatMatrix3dValues([
1486
+ xCol[0],
1487
+ xCol[1],
1488
+ xCol[2],
1489
+ 0,
1490
+ yCol[0],
1491
+ yCol[1],
1492
+ yCol[2],
1493
+ 0,
1494
+ normal[0],
1495
+ normal[1],
1496
+ normal[2],
1497
+ 0,
1498
+ txCol[0],
1499
+ txCol[1],
1500
+ txCol[2],
1501
+ 1
1502
+ ]);
1503
+ return {
1504
+ transform: `matrix3d(${canonicalMatrix})`,
1505
+ ...sharedStyle
1506
+ };
885
1507
  }
886
1508
  function rgbToHex({ r, g, b }) {
887
1509
  const f = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
@@ -1024,6 +1646,15 @@ function expandClipPoints(points, amount) {
1024
1646
  }
1025
1647
  return expanded;
1026
1648
  }
1649
+ function tracePolygonPath(ctx, x, y, points) {
1650
+ for (let i = 0; i < points.length; i += 2) {
1651
+ const px = x + points[i];
1652
+ const py = y + points[i + 1];
1653
+ if (i === 0) ctx.moveTo(px, py);
1654
+ else ctx.lineTo(px, py);
1655
+ }
1656
+ ctx.closePath();
1657
+ }
1027
1658
  function canvasToUrl(canvas) {
1028
1659
  if (typeof canvas.toBlob === "function") {
1029
1660
  return new Promise((resolve) => {
@@ -1067,6 +1698,176 @@ function drawImageUvSample(ctx, img, rect, x, y, width, height, atlasScale) {
1067
1698
  setCssTransform(ctx, atlasScale);
1068
1699
  ctx.drawImage(img, sx, sy, sw, sh, x, y, width, height);
1069
1700
  }
1701
+ function traceOffsetPolygonPath(ctx, x, y, points, offsetX, offsetY) {
1702
+ for (let i = 0; i < points.length; i += 2) {
1703
+ const px = x + points[i] + offsetX;
1704
+ const py = y + points[i + 1] + offsetY;
1705
+ if (i === 0) ctx.moveTo(px, py);
1706
+ else ctx.lineTo(px, py);
1707
+ }
1708
+ ctx.closePath();
1709
+ }
1710
+ function drawTexturedAtlasEntry(ctx, entry, srcImg, atlasScale, offsetX = 0, offsetY = 0) {
1711
+ if (entry.textureTriangles?.length) {
1712
+ const imgW = srcImg.naturalWidth || srcImg.width || 1;
1713
+ const imgH = srcImg.naturalHeight || srcImg.height || 1;
1714
+ for (const triangle of entry.textureTriangles) {
1715
+ const clipPts = expandClipPoints(triangle.screenPts, TEXTURE_TRIANGLE_BLEED);
1716
+ ctx.save();
1717
+ setCssTransform(ctx, atlasScale);
1718
+ ctx.beginPath();
1719
+ traceOffsetPolygonPath(ctx, entry.x, entry.y, clipPts, offsetX, offsetY);
1720
+ ctx.clip();
1721
+ if (triangle.uvAffine) {
1722
+ setCssTransform(
1723
+ ctx,
1724
+ atlasScale,
1725
+ triangle.uvAffine.a / imgW,
1726
+ triangle.uvAffine.c / imgW,
1727
+ triangle.uvAffine.b / imgH,
1728
+ triangle.uvAffine.d / imgH,
1729
+ entry.x + triangle.uvAffine.e + offsetX,
1730
+ entry.y + triangle.uvAffine.f + offsetY
1731
+ );
1732
+ ctx.drawImage(srcImg, 0, 0);
1733
+ } else if (triangle.uvSampleRect) {
1734
+ drawImageUvSample(
1735
+ ctx,
1736
+ srcImg,
1737
+ triangle.uvSampleRect,
1738
+ entry.x + offsetX,
1739
+ entry.y + offsetY,
1740
+ entry.canvasW,
1741
+ entry.canvasH,
1742
+ atlasScale
1743
+ );
1744
+ }
1745
+ ctx.restore();
1746
+ }
1747
+ } else if (entry.uvAffine) {
1748
+ const imgW = srcImg.naturalWidth || srcImg.width || 1;
1749
+ const imgH = srcImg.naturalHeight || srcImg.height || 1;
1750
+ setCssTransform(
1751
+ ctx,
1752
+ atlasScale,
1753
+ entry.uvAffine.a / imgW,
1754
+ entry.uvAffine.c / imgW,
1755
+ entry.uvAffine.b / imgH,
1756
+ entry.uvAffine.d / imgH,
1757
+ entry.x + entry.uvAffine.e + offsetX,
1758
+ entry.y + entry.uvAffine.f + offsetY
1759
+ );
1760
+ ctx.drawImage(srcImg, 0, 0);
1761
+ } else if (entry.uvSampleRect) {
1762
+ drawImageUvSample(
1763
+ ctx,
1764
+ srcImg,
1765
+ entry.uvSampleRect,
1766
+ entry.x + offsetX,
1767
+ entry.y + offsetY,
1768
+ entry.canvasW,
1769
+ entry.canvasH,
1770
+ atlasScale
1771
+ );
1772
+ } else {
1773
+ drawImageCover(
1774
+ ctx,
1775
+ srcImg,
1776
+ entry.x + offsetX,
1777
+ entry.y + offsetY,
1778
+ entry.canvasW,
1779
+ entry.canvasH,
1780
+ atlasScale
1781
+ );
1782
+ }
1783
+ }
1784
+ function distanceToSegment(px, py, ax, ay, bx, by) {
1785
+ const dx = bx - ax;
1786
+ const dy = by - ay;
1787
+ const lenSq = dx * dx + dy * dy;
1788
+ if (lenSq <= BASIS_EPS) return Math.hypot(px - ax, py - ay);
1789
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
1790
+ return Math.hypot(px - (ax + dx * t), py - (ay + dy * t));
1791
+ }
1792
+ function distanceToPolygonEdges(px, py, points, edgeIndices) {
1793
+ let best = Infinity;
1794
+ const count = points.length / 2;
1795
+ for (const edgeIndex of edgeIndices) {
1796
+ if (edgeIndex < 0 || edgeIndex >= count) continue;
1797
+ const i = edgeIndex * 2;
1798
+ const next = (edgeIndex + 1) % count * 2;
1799
+ best = Math.min(
1800
+ best,
1801
+ distanceToSegment(px, py, points[i], points[i + 1], points[next], points[next + 1])
1802
+ );
1803
+ }
1804
+ return best;
1805
+ }
1806
+ function nearestOpaquePixelOffset(data, width, height, x, y, radius) {
1807
+ const minX = Math.max(0, x - radius);
1808
+ const maxX = Math.min(width - 1, x + radius);
1809
+ const minY = Math.max(0, y - radius);
1810
+ const maxY = Math.min(height - 1, y + radius);
1811
+ let bestOffset = null;
1812
+ let bestDistanceSq = Infinity;
1813
+ for (let yy = minY; yy <= maxY; yy++) {
1814
+ for (let xx = minX; xx <= maxX; xx++) {
1815
+ if (xx === x && yy === y) continue;
1816
+ const dx = xx - x;
1817
+ const dy = yy - y;
1818
+ const distanceSq = dx * dx + dy * dy;
1819
+ if (distanceSq > radius * radius || distanceSq >= bestDistanceSq) continue;
1820
+ const offset = (yy * width + xx) * 4;
1821
+ if (data[offset + 3] < TEXTURE_EDGE_REPAIR_SOURCE_ALPHA_MIN) continue;
1822
+ bestOffset = offset;
1823
+ bestDistanceSq = distanceSq;
1824
+ }
1825
+ }
1826
+ return bestOffset;
1827
+ }
1828
+ function repairTextureEdgeAlpha(ctx, entry, atlasScale) {
1829
+ if (!entry.textureEdgeRepair || !entry.texture) return;
1830
+ if (!entry.textureEdgeRepairEdges || entry.textureEdgeRepairEdges.size === 0) return;
1831
+ const canvas = ctx.canvas;
1832
+ if (!canvas) return;
1833
+ const pixelX = Math.max(0, Math.floor(entry.x * atlasScale));
1834
+ const pixelY = Math.max(0, Math.floor(entry.y * atlasScale));
1835
+ const pixelW = Math.max(1, Math.min(canvas.width - pixelX, Math.ceil(entry.canvasW * atlasScale)));
1836
+ const pixelH = Math.max(1, Math.min(canvas.height - pixelY, Math.ceil(entry.canvasH * atlasScale)));
1837
+ if (pixelW <= 0 || pixelH <= 0) return;
1838
+ let imageData;
1839
+ try {
1840
+ imageData = ctx.getImageData(pixelX, pixelY, pixelW, pixelH);
1841
+ } catch {
1842
+ return;
1843
+ }
1844
+ const data = imageData.data;
1845
+ const source = new Uint8ClampedArray(data);
1846
+ const radius = Math.max(TEXTURE_EDGE_REPAIR_RADIUS, TEXTURE_EDGE_REPAIR_RADIUS / atlasScale);
1847
+ const sourceRadius = Math.max(2, Math.ceil(radius * atlasScale) + 1);
1848
+ let changed = false;
1849
+ for (let y = 0; y < pixelH; y++) {
1850
+ for (let x = 0; x < pixelW; x++) {
1851
+ const offset = (y * pixelW + x) * 4;
1852
+ const alpha = data[offset + 3];
1853
+ if (alpha < TEXTURE_EDGE_REPAIR_ALPHA_MIN || alpha === 255) continue;
1854
+ const localX = (pixelX + x + 0.5) / atlasScale - entry.x;
1855
+ const localY = (pixelY + y + 0.5) / atlasScale - entry.y;
1856
+ if (distanceToPolygonEdges(localX, localY, entry.screenPts, entry.textureEdgeRepairEdges) > radius) {
1857
+ continue;
1858
+ }
1859
+ const sourceOffset = nearestOpaquePixelOffset(source, pixelW, pixelH, x, y, sourceRadius);
1860
+ if (sourceOffset === null) continue;
1861
+ data[offset] = source[sourceOffset];
1862
+ data[offset + 1] = source[sourceOffset + 1];
1863
+ data[offset + 2] = source[sourceOffset + 2];
1864
+ data[offset + 3] = 255;
1865
+ changed = true;
1866
+ }
1867
+ }
1868
+ if (!changed) return;
1869
+ ctx.putImageData(imageData, pixelX, pixelY);
1870
+ }
1070
1871
  function computeTextureAtlasPlan(polygon, index, options = {}) {
1071
1872
  const { vertices, texture, uvs } = polygon;
1072
1873
  if (!vertices || vertices.length < 3) return null;
@@ -1113,18 +1914,20 @@ function computeTextureAtlasPlan(polygon, index, options = {}) {
1113
1914
  if (y < yMin) yMin = y;
1114
1915
  if (y > yMax) yMax = y;
1115
1916
  }
1116
- const shiftX = -xMin;
1117
- const shiftY = -yMin;
1118
1917
  const w = xMax - xMin;
1119
1918
  const h = yMax - yMin;
1120
1919
  if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
1920
+ const textureEdgeRepairEdges = texture && options.textureEdgeRepairEdges?.size ? options.textureEdgeRepairEdges : null;
1921
+ const textureEdgeRepair = Boolean(texture && textureEdgeRepairEdges);
1922
+ const shiftX = -xMin;
1923
+ const shiftY = -yMin;
1121
1924
  const screenPts = [];
1122
1925
  for (let i = 0; i < local2D.length; i++) {
1123
1926
  const [x, y] = local2D[i];
1124
- const sx = x + shiftX;
1125
- const sy = y + shiftY;
1126
- screenPts.push(sx, sy);
1927
+ screenPts.push(x + shiftX, y + shiftY);
1127
1928
  }
1929
+ const canvasW = Math.max(1, Math.ceil(w));
1930
+ const canvasH = Math.max(1, Math.ceil(h));
1128
1931
  const tx = p0[0] - shiftX * xAxis[0] - shiftY * yAxis[0];
1129
1932
  const ty = p0[1] - shiftX * xAxis[1] - shiftY * yAxis[1];
1130
1933
  const tz = p0[2] - shiftX * xAxis[2] - shiftY * yAxis[2];
@@ -1146,6 +1949,44 @@ function computeTextureAtlasPlan(polygon, index, options = {}) {
1146
1949
  tz,
1147
1950
  1
1148
1951
  ].join(",");
1952
+ const canonicalMatrix = [
1953
+ xAxis[0] * canvasW,
1954
+ xAxis[1] * canvasW,
1955
+ xAxis[2] * canvasW,
1956
+ 0,
1957
+ yAxis[0] * canvasH,
1958
+ yAxis[1] * canvasH,
1959
+ yAxis[2] * canvasH,
1960
+ 0,
1961
+ nx,
1962
+ ny,
1963
+ nz,
1964
+ 0,
1965
+ tx,
1966
+ ty,
1967
+ tz,
1968
+ 1
1969
+ ].join(",");
1970
+ const atlasMatrix = [
1971
+ xAxis[0] * canvasW / ATLAS_CANONICAL_SIZE_EXPLICIT,
1972
+ xAxis[1] * canvasW / ATLAS_CANONICAL_SIZE_EXPLICIT,
1973
+ xAxis[2] * canvasW / ATLAS_CANONICAL_SIZE_EXPLICIT,
1974
+ 0,
1975
+ yAxis[0] * canvasH / ATLAS_CANONICAL_SIZE_EXPLICIT,
1976
+ yAxis[1] * canvasH / ATLAS_CANONICAL_SIZE_EXPLICIT,
1977
+ yAxis[2] * canvasH / ATLAS_CANONICAL_SIZE_EXPLICIT,
1978
+ 0,
1979
+ nx,
1980
+ ny,
1981
+ nz,
1982
+ 0,
1983
+ tx,
1984
+ ty,
1985
+ tz,
1986
+ 1
1987
+ ].join(",");
1988
+ const normal = [nx, ny, nz];
1989
+ const projectiveMatrix = !texture && vertices.length === 4 ? computeProjectiveQuadMatrix(screenPts, xAxis, yAxis, normal, tx, ty, tz) : null;
1149
1990
  const directionalCfg = options.directionalLight;
1150
1991
  const ambientCfg = options.ambientLight;
1151
1992
  const lightDir = directionalCfg?.direction ?? DEFAULT_LIGHT_DIR;
@@ -1177,13 +2018,18 @@ function computeTextureAtlasPlan(polygon, index, options = {}) {
1177
2018
  tileSize: tile,
1178
2019
  layerElevation: elev,
1179
2020
  matrix,
1180
- canvasW: Math.max(1, Math.ceil(w)),
1181
- canvasH: Math.max(1, Math.ceil(h)),
2021
+ canonicalMatrix,
2022
+ atlasMatrix,
2023
+ projectiveMatrix,
2024
+ canvasW,
2025
+ canvasH,
1182
2026
  screenPts,
1183
2027
  uvAffine,
1184
2028
  uvSampleRect,
1185
2029
  textureTriangles,
1186
- normal: [nx, ny, nz],
2030
+ textureEdgeRepairEdges,
2031
+ textureEdgeRepair,
2032
+ normal,
1187
2033
  textureTint,
1188
2034
  shadedColor
1189
2035
  };
@@ -1267,7 +2113,10 @@ async function buildAtlasPage(page, textureLighting, doc, atlasScale) {
1267
2113
  const canvas = doc.createElement("canvas");
1268
2114
  canvas.width = Math.max(1, Math.ceil(page.width * atlasScale));
1269
2115
  canvas.height = Math.max(1, Math.ceil(page.height * atlasScale));
1270
- const ctx = canvas.getContext("2d");
2116
+ const needsReadback = page.entries.some(
2117
+ (entry) => entry.textureEdgeRepair && entry.texture && entry.textureEdgeRepairEdges && entry.textureEdgeRepairEdges.size > 0
2118
+ );
2119
+ const ctx = canvas.getContext("2d", needsReadback ? { willReadFrequently: true } : void 0);
1271
2120
  if (!ctx) return { width: page.width, height: page.height, url: null };
1272
2121
  const uniqueTextures = Array.from(new Set(
1273
2122
  page.entries.flatMap((entry) => entry.texture ? [entry.texture] : [])
@@ -1278,97 +2127,36 @@ async function buildAtlasPage(page, textureLighting, doc, atlasScale) {
1278
2127
  }));
1279
2128
  for (const entry of page.entries) {
1280
2129
  const srcImg = entry.texture ? loaded.get(entry.texture) : null;
1281
- ctx.save();
1282
- setCssTransform(ctx, atlasScale);
1283
- ctx.beginPath();
1284
- for (let i = 0; i < entry.screenPts.length; i += 2) {
1285
- const px = entry.x + entry.screenPts[i];
1286
- const py = entry.y + entry.screenPts[i + 1];
1287
- if (i === 0) ctx.moveTo(px, py);
1288
- else ctx.lineTo(px, py);
1289
- }
1290
- ctx.closePath();
1291
- ctx.clip();
1292
2130
  if (!entry.texture) {
2131
+ ctx.save();
1293
2132
  setCssTransform(ctx, atlasScale);
2133
+ ctx.beginPath();
2134
+ tracePolygonPath(ctx, entry.x, entry.y, entry.screenPts);
2135
+ ctx.clip();
1294
2136
  ctx.fillStyle = textureLighting === "dynamic" ? entry.polygon.color ?? "#cccccc" : entry.shadedColor;
1295
2137
  ctx.fillRect(entry.x, entry.y, entry.canvasW, entry.canvasH);
1296
- } else {
1297
- if (srcImg && entry.textureTriangles?.length) {
1298
- const imgW = srcImg.naturalWidth || srcImg.width || 1;
1299
- const imgH = srcImg.naturalHeight || srcImg.height || 1;
1300
- for (const triangle of entry.textureTriangles) {
1301
- const clipPts = expandClipPoints(triangle.screenPts, TEXTURE_TRIANGLE_BLEED);
1302
- ctx.save();
1303
- setCssTransform(ctx, atlasScale);
1304
- ctx.beginPath();
1305
- for (let i = 0; i < clipPts.length; i += 2) {
1306
- const px = entry.x + clipPts[i];
1307
- const py = entry.y + clipPts[i + 1];
1308
- if (i === 0) ctx.moveTo(px, py);
1309
- else ctx.lineTo(px, py);
1310
- }
1311
- ctx.closePath();
1312
- ctx.clip();
1313
- if (triangle.uvAffine) {
1314
- setCssTransform(
1315
- ctx,
1316
- atlasScale,
1317
- triangle.uvAffine.a / imgW,
1318
- triangle.uvAffine.c / imgW,
1319
- triangle.uvAffine.b / imgH,
1320
- triangle.uvAffine.d / imgH,
1321
- entry.x + triangle.uvAffine.e,
1322
- entry.y + triangle.uvAffine.f
1323
- );
1324
- ctx.drawImage(srcImg, 0, 0);
1325
- } else if (triangle.uvSampleRect) {
1326
- drawImageUvSample(
1327
- ctx,
1328
- srcImg,
1329
- triangle.uvSampleRect,
1330
- entry.x,
1331
- entry.y,
1332
- entry.canvasW,
1333
- entry.canvasH,
1334
- atlasScale
1335
- );
1336
- }
1337
- ctx.restore();
1338
- }
1339
- } else if (srcImg && entry.uvAffine) {
1340
- const imgW = srcImg.naturalWidth || srcImg.width || 1;
1341
- const imgH = srcImg.naturalHeight || srcImg.height || 1;
1342
- setCssTransform(
1343
- ctx,
1344
- atlasScale,
1345
- entry.uvAffine.a / imgW,
1346
- entry.uvAffine.c / imgW,
1347
- entry.uvAffine.b / imgH,
1348
- entry.uvAffine.d / imgH,
1349
- entry.x + entry.uvAffine.e,
1350
- entry.y + entry.uvAffine.f
1351
- );
1352
- ctx.drawImage(srcImg, 0, 0);
1353
- } else if (srcImg && entry.uvSampleRect) {
1354
- drawImageUvSample(
1355
- ctx,
1356
- srcImg,
1357
- entry.uvSampleRect,
1358
- entry.x,
1359
- entry.y,
1360
- entry.canvasW,
1361
- entry.canvasH,
1362
- atlasScale
1363
- );
1364
- } else if (srcImg) {
1365
- drawImageCover(ctx, srcImg, entry.x, entry.y, entry.canvasW, entry.canvasH, atlasScale);
1366
- }
2138
+ ctx.restore();
2139
+ continue;
2140
+ }
2141
+ if (srcImg) {
2142
+ ctx.save();
2143
+ setCssTransform(ctx, atlasScale);
2144
+ ctx.beginPath();
2145
+ tracePolygonPath(ctx, entry.x, entry.y, entry.screenPts);
2146
+ ctx.clip();
2147
+ drawTexturedAtlasEntry(ctx, entry, srcImg, atlasScale);
2148
+ ctx.restore();
1367
2149
  }
1368
2150
  if (entry.texture && textureLighting === "baked") {
2151
+ ctx.save();
2152
+ setCssTransform(ctx, atlasScale);
2153
+ ctx.beginPath();
2154
+ tracePolygonPath(ctx, entry.x, entry.y, entry.screenPts);
2155
+ ctx.clip();
1369
2156
  applyTextureTint(ctx, entry.x, entry.y, entry.canvasW, entry.canvasH, entry.textureTint, atlasScale);
2157
+ ctx.restore();
1370
2158
  }
1371
- ctx.restore();
2159
+ repairTextureEdgeAlpha(ctx, entry, atlasScale);
1372
2160
  }
1373
2161
  const url = await canvasToUrl(canvas);
1374
2162
  canvas.width = 1;
@@ -1387,21 +2175,32 @@ async function buildAtlasPages(pages, textureLighting, doc, atlasScale, isCancel
1387
2175
  }
1388
2176
  return built;
1389
2177
  }
1390
- function useTextureAtlas(plans, textureLighting, atlasScaleInput) {
1391
- const useBorderShape = textureLighting !== "dynamic" && borderShapeSupported();
2178
+ function useTextureAtlas(plans, textureLighting, textureQualityInput, strategies) {
2179
+ const disableB = strategies?.disable?.includes("b") ?? false;
2180
+ const disableI = strategies?.disable?.includes("i") ?? false;
2181
+ const disableU = strategies?.disable?.includes("u") ?? false;
2182
+ const useFullRectSolid = !disableB;
2183
+ const useProjectiveQuad = useFullRectSolid;
2184
+ const useStableTriangle = !disableU && solidTriangleSupported();
2185
+ const useBorderShape = !disableI && textureLighting !== "dynamic" && borderShapeSupported();
1392
2186
  const atlasPlans = (0, import_react7.useMemo)(
1393
2187
  () => plans.map((plan) => {
1394
2188
  if (!plan) return plan;
1395
2189
  if (plan.texture) return plan;
1396
- if (isSolidTrianglePlan(plan)) return null;
1397
- if (textureLighting !== "dynamic" && (useBorderShape || isFullRectSolid(plan))) return null;
2190
+ if (useStableTriangle && isSolidTrianglePlan(plan)) return null;
2191
+ const fullRect = isFullRectSolid(plan);
2192
+ if (useFullRectSolid && fullRect || useProjectiveQuad && isProjectiveQuadPlan(plan) || textureLighting !== "dynamic" && useBorderShape && (!fullRect || disableB)) return null;
1398
2193
  return plan;
1399
2194
  }),
1400
- [plans, textureLighting, useBorderShape]
2195
+ [plans, textureLighting, useFullRectSolid, useProjectiveQuad, useStableTriangle, useBorderShape]
1401
2196
  );
1402
2197
  const { packed, atlasScale } = (0, import_react7.useMemo)(
1403
- () => packTextureAtlasPlansWithScale(atlasPlans, atlasScaleInput),
1404
- [atlasPlans, atlasScaleInput]
2198
+ () => packTextureAtlasPlansWithScale(
2199
+ atlasPlans,
2200
+ textureQualityInput,
2201
+ typeof document !== "undefined" ? document : null
2202
+ ),
2203
+ [atlasPlans, textureQualityInput]
1405
2204
  );
1406
2205
  const [pages, setPages] = (0, import_react7.useState)(
1407
2206
  () => packed.pages.map((page) => ({ width: page.width, height: page.height, url: null }))
@@ -1441,31 +2240,34 @@ function useTextureAtlas(plans, textureLighting, atlasScaleInput) {
1441
2240
  }
1442
2241
  function TextureBorderShapePoly({
1443
2242
  entry,
2243
+ solidPaintDefaults,
1444
2244
  className,
1445
2245
  style: styleProp,
1446
2246
  domAttrs,
1447
2247
  domEventHandlers,
1448
- pointerEvents = "auto"
2248
+ pointerEvents = "auto",
2249
+ disabledStrategies
1449
2250
  }) {
1450
2251
  const fullRect = isFullRectSolid(entry);
1451
- const borderShape = fullRect ? null : cssBorderShapeForPlan(entry);
2252
+ const bDisabled = disabledStrategies?.has("b") ?? false;
2253
+ const useIForFullRect = bDisabled && borderShapeSupported();
2254
+ const borderShape = !fullRect || useIForFullRect ? cssBorderShapeForPlan(entry) : null;
2255
+ const useDefaultPaint = entry.shadedColor === solidPaintDefaults?.paintColor;
1452
2256
  const setElementRef = (0, import_react7.useCallback)((el) => {
1453
2257
  if (!el) return;
1454
2258
  if (borderShape) el.style.setProperty("border-shape", borderShape);
1455
2259
  else el.style.removeProperty("border-shape");
2260
+ orderBrushInlineStyle(el);
1456
2261
  }, [borderShape]);
2262
+ const transform = formatMatrix3d(borderShape ? formatBorderShapeMatrix(entry) : formatSolidQuadMatrix(entry));
1457
2263
  const style = fullRect ? {
1458
- width: entry.canvasW,
1459
- height: entry.canvasH,
1460
- transform: formatMatrix3d(entry.matrix),
1461
- background: entry.shadedColor,
2264
+ transform,
2265
+ color: useDefaultPaint ? void 0 : entry.shadedColor,
1462
2266
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
1463
2267
  ...styleProp
1464
2268
  } : {
1465
- width: entry.canvasW,
1466
- height: entry.canvasH,
1467
- transform: formatMatrix3d(entry.matrix),
1468
- color: entry.shadedColor,
2269
+ transform,
2270
+ color: useDefaultPaint ? void 0 : entry.shadedColor,
1469
2271
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
1470
2272
  ...styleProp
1471
2273
  };
@@ -1473,7 +2275,7 @@ function TextureBorderShapePoly({
1473
2275
  Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
1474
2276
  ) : {};
1475
2277
  const elementClassName = className?.trim() || void 0;
1476
- if (fullRect) {
2278
+ if (fullRect && !useIForFullRect) {
1477
2279
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1478
2280
  "b",
1479
2281
  {
@@ -1497,16 +2299,63 @@ function TextureBorderShapePoly({
1497
2299
  }
1498
2300
  );
1499
2301
  }
2302
+ function TextureProjectiveSolidPoly({
2303
+ entry,
2304
+ textureLighting,
2305
+ solidPaintDefaults,
2306
+ className,
2307
+ style: styleProp,
2308
+ domAttrs,
2309
+ domEventHandlers,
2310
+ pointerEvents = "auto"
2311
+ }) {
2312
+ const dynamic = textureLighting === "dynamic";
2313
+ const base = parseHex(entry.polygon.color ?? "#cccccc");
2314
+ const useDefaultDynamicColor = dynamic && rgbKey(base) === solidPaintDefaults?.dynamicColorKey;
2315
+ const style = {
2316
+ transform: formatMatrix3d(entry.projectiveMatrix),
2317
+ color: dynamic || entry.shadedColor === solidPaintDefaults?.paintColor ? void 0 : entry.shadedColor,
2318
+ pointerEvents: pointerEvents === "none" ? "none" : void 0,
2319
+ ...dynamic && !useDefaultDynamicColor ? {
2320
+ ["--pnx"]: entry.normal[0].toFixed(4),
2321
+ ["--pny"]: entry.normal[1].toFixed(4),
2322
+ ["--pnz"]: entry.normal[2].toFixed(4),
2323
+ ["--psr"]: (base.r / 255).toFixed(4),
2324
+ ["--psg"]: (base.g / 255).toFixed(4),
2325
+ ["--psb"]: (base.b / 255).toFixed(4)
2326
+ } : dynamic ? {
2327
+ ["--pnx"]: entry.normal[0].toFixed(4),
2328
+ ["--pny"]: entry.normal[1].toFixed(4),
2329
+ ["--pnz"]: entry.normal[2].toFixed(4)
2330
+ } : null,
2331
+ ...styleProp
2332
+ };
2333
+ const dataAttrs = entry.polygon.data ? Object.fromEntries(
2334
+ Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
2335
+ ) : {};
2336
+ const elementClassName = className?.trim() || void 0;
2337
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2338
+ "b",
2339
+ {
2340
+ className: elementClassName,
2341
+ style,
2342
+ ...domEventHandlers,
2343
+ ...dataAttrs,
2344
+ ...domAttrs
2345
+ }
2346
+ );
2347
+ }
1500
2348
  function TextureTrianglePoly({
1501
2349
  entry,
1502
2350
  textureLighting,
2351
+ solidPaintDefaults,
1503
2352
  className,
1504
2353
  style: styleProp,
1505
2354
  domAttrs,
1506
2355
  domEventHandlers,
1507
2356
  pointerEvents = "auto"
1508
2357
  }) {
1509
- const triangleStyle = solidTriangleStyle(entry, textureLighting, pointerEvents);
2358
+ const triangleStyle = solidTriangleStyle(entry, textureLighting, pointerEvents, solidPaintDefaults);
1510
2359
  if (!triangleStyle) return null;
1511
2360
  const dataAttrs = entry.polygon.data ? Object.fromEntries(
1512
2361
  Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
@@ -1534,16 +2383,20 @@ function TextureAtlasPoly({
1534
2383
  pointerEvents = "auto"
1535
2384
  }) {
1536
2385
  const dynamic = textureLighting === "dynamic";
2386
+ const atlasCanonicalSize = entry.atlasCanonicalSize ?? ATLAS_CANONICAL_SIZE_EXPLICIT;
2387
+ const atlasWidth = entry.canvasW || 1;
2388
+ const atlasHeight = entry.canvasH || 1;
2389
+ const atlasPosition = page ? `${formatCssLength(-entry.x / atlasWidth * atlasCanonicalSize)} ${formatCssLength(-entry.y / atlasHeight * atlasCanonicalSize)}` : void 0;
2390
+ const atlasSize = page ? `${formatCssLength(page.width / atlasWidth * atlasCanonicalSize)} ${formatCssLength(page.height / atlasHeight * atlasCanonicalSize)}` : void 0;
1537
2391
  const dynamicMask = dynamic && page?.url ? `url(${page.url})` : void 0;
1538
- const background = !dynamic && page?.url ? `url(${page.url}) -${entry.x}px -${entry.y}px / ${page.width}px ${page.height}px no-repeat` : void 0;
2392
+ const background = !dynamic && page?.url ? `url(${page.url}) ${atlasPosition} / ${atlasSize} no-repeat` : void 0;
1539
2393
  const style = {
1540
- width: entry.canvasW,
1541
- height: entry.canvasH,
1542
- transform: formatMatrix3d(entry.matrix),
2394
+ transform: formatMatrix3d(entry.atlasMatrix),
2395
+ ["--polycss-atlas-size"]: `${atlasCanonicalSize}px`,
1543
2396
  background,
1544
2397
  backgroundImage: dynamic && page?.url ? `url(${page.url})` : void 0,
1545
- backgroundPosition: dynamic ? `-${entry.x}px -${entry.y}px` : void 0,
1546
- backgroundSize: dynamic && page ? `${page.width}px ${page.height}px` : void 0,
2398
+ backgroundPosition: dynamic ? atlasPosition : void 0,
2399
+ backgroundSize: dynamic ? atlasSize : void 0,
1547
2400
  ...dynamic ? {
1548
2401
  ["--pnx"]: entry.normal[0].toFixed(4),
1549
2402
  ["--pny"]: entry.normal[1].toFixed(4),
@@ -1554,12 +2407,12 @@ function TextureAtlasPoly({
1554
2407
  // the polygon don't get painted with the tint.
1555
2408
  maskImage: dynamicMask,
1556
2409
  maskMode: "alpha",
1557
- maskPosition: `-${entry.x}px -${entry.y}px`,
1558
- maskSize: page ? `${page.width}px ${page.height}px` : void 0,
2410
+ maskPosition: atlasPosition,
2411
+ maskSize: atlasSize,
1559
2412
  maskRepeat: "no-repeat",
1560
2413
  WebkitMaskImage: dynamicMask,
1561
- WebkitMaskPosition: `-${entry.x}px -${entry.y}px`,
1562
- WebkitMaskSize: page ? `${page.width}px ${page.height}px` : void 0,
2414
+ WebkitMaskPosition: atlasPosition,
2415
+ WebkitMaskSize: atlasSize,
1563
2416
  WebkitMaskRepeat: "no-repeat"
1564
2417
  } : null,
1565
2418
  opacity: page?.url ? void 0 : 0,
@@ -1601,8 +2454,10 @@ function PolySceneInner({
1601
2454
  directionalLight,
1602
2455
  ambientLight,
1603
2456
  textureLighting = "baked",
1604
- atlasScale,
2457
+ textureQuality,
2458
+ strategies,
1605
2459
  autoCenter = false,
2460
+ shadow,
1606
2461
  className,
1607
2462
  style,
1608
2463
  children,
@@ -1646,14 +2501,6 @@ function PolySceneInner({
1646
2501
  { directionalLight }
1647
2502
  );
1648
2503
  const sceneBbox = centerInputPolygons ? centerSceneBbox : renderSceneBbox;
1649
- const sceneStyle = (0, import_react9.useMemo)(() => {
1650
- const handle = (0, import_polycss_core4.createIsometricCamera)(cameraState);
1651
- const cameraStyle = handle.getStyle();
1652
- return {
1653
- ["--scene-transform"]: cameraStyle.transform
1654
- };
1655
- }, [cameraState]);
1656
- const computedClassName = `polycss-scene${className ? ` ${className}` : ""}`;
1657
2504
  const directionalForAtlas = textureLighting === "dynamic" ? void 0 : directionalLight;
1658
2505
  const ambientForAtlas = textureLighting === "dynamic" ? void 0 : ambientLight;
1659
2506
  const polyContext = (0, import_react9.useMemo)(() => {
@@ -1666,11 +2513,43 @@ function PolySceneInner({
1666
2513
  textureLighting
1667
2514
  };
1668
2515
  }, [directionalForAtlas, ambientForAtlas, textureLighting]);
2516
+ const autoCenterOffset = (0, import_react9.useMemo)(() => {
2517
+ if (!autoCenter) return [0, 0, 0];
2518
+ return [
2519
+ (sceneBbox.min[0] + sceneBbox.max[0]) / 2,
2520
+ (sceneBbox.min[1] + sceneBbox.max[1]) / 2,
2521
+ (sceneBbox.min[2] + sceneBbox.max[2]) / 2
2522
+ ];
2523
+ }, [autoCenter, sceneBbox]);
2524
+ (0, import_react9.useEffect)(() => {
2525
+ store.setAutoCenterOffset(autoCenterOffset);
2526
+ }, [store, autoCenterOffset]);
2527
+ const sceneStyle = (0, import_react9.useMemo)(() => {
2528
+ const s = cameraState;
2529
+ const [ox, oy, oz] = autoCenterOffset;
2530
+ const tileSize = import_polycss_core4.BASE_TILE;
2531
+ const wx = s.target[0] + ox;
2532
+ const wy = s.target[1] + oy;
2533
+ const wz = s.target[2] + oz;
2534
+ const cssX = wy * tileSize;
2535
+ const cssY = wx * tileSize;
2536
+ const cssZ = wz * tileSize;
2537
+ const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
2538
+ const transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
2539
+ return { transform };
2540
+ }, [cameraState, autoCenterOffset]);
2541
+ const computedClassName = `polycss-scene${className ? ` ${className}` : ""}`;
1669
2542
  const textureAtlasPlans = (0, import_react9.useMemo)(
1670
- () => polygons.map((p, i) => computeTextureAtlasPlan(p, i, polyContext)),
2543
+ () => {
2544
+ const repairEdges = buildTextureEdgeRepairSets(polygons);
2545
+ return polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
2546
+ ...polyContext,
2547
+ textureEdgeRepairEdges: repairEdges[i]
2548
+ }));
2549
+ },
1671
2550
  [polygons, polyContext]
1672
2551
  );
1673
- const textureAtlas = useTextureAtlas(textureAtlasPlans, textureLighting, atlasScale);
2552
+ const textureAtlas = useTextureAtlas(textureAtlasPlans, textureLighting, textureQuality, strategies);
1674
2553
  const dynamicLightVars = (0, import_react9.useMemo)(() => {
1675
2554
  if (textureLighting !== "dynamic") return null;
1676
2555
  const dir = directionalLight?.direction ?? [0.4, -0.7, 0.59];
@@ -1681,6 +2560,8 @@ function PolySceneInner({
1681
2560
  const lightIntensity = directionalLight?.intensity ?? 1;
1682
2561
  const ambientIntensity = ambientLight?.intensity ?? 0.4;
1683
2562
  const ch = (n) => (n / 255).toFixed(4);
2563
+ const rawClz = lz;
2564
+ const clz = Math.sign(rawClz || 1) * Math.max(Math.abs(rawClz), 0.01);
1684
2565
  return {
1685
2566
  ["--plx"]: lx.toFixed(4),
1686
2567
  ["--ply"]: ly.toFixed(4),
@@ -1692,16 +2573,52 @@ function PolySceneInner({
1692
2573
  ["--par"]: ch(ambRgb[0]),
1693
2574
  ["--pag"]: ch(ambRgb[1]),
1694
2575
  ["--pab"]: ch(ambRgb[2]),
1695
- ["--pai"]: ambientIntensity.toFixed(4)
2576
+ ["--pai"]: ambientIntensity.toFixed(4),
2577
+ ["--clx"]: lx.toFixed(4),
2578
+ ["--cly"]: ly.toFixed(4),
2579
+ ["--clz"]: clz.toFixed(4)
1696
2580
  };
1697
2581
  }, [textureLighting, directionalLight, ambientLight]);
1698
- const autoCenterTransform = (0, import_react9.useMemo)(() => {
1699
- if (!autoCenter) return void 0;
1700
- const cssX = (sceneBbox.min[1] + sceneBbox.max[1]) / 2 * polyContext.tileSize;
1701
- const cssY = (sceneBbox.min[0] + sceneBbox.max[0]) / 2 * polyContext.tileSize;
1702
- const cssZ = (sceneBbox.min[2] + sceneBbox.max[2]) / 2 * polyContext.layerElevation;
1703
- return `translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
1704
- }, [autoCenter, sceneBbox, polyContext.tileSize, polyContext.layerElevation]);
2582
+ const shadowCastersRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
2583
+ const registerShadowCaster = (0, import_react9.useCallback)((meshId, meshPolygons) => {
2584
+ if (meshPolygons === null) {
2585
+ shadowCastersRef.current.delete(meshId);
2586
+ } else {
2587
+ shadowCastersRef.current.set(meshId, meshPolygons);
2588
+ }
2589
+ const el = sceneElRef.current;
2590
+ if (!el) return;
2591
+ if (textureLighting !== "dynamic") {
2592
+ el.style.removeProperty("--shadow-ground-cssz");
2593
+ return;
2594
+ }
2595
+ let minWorldZ = Infinity;
2596
+ for (const polys of shadowCastersRef.current.values()) {
2597
+ for (const poly of polys) {
2598
+ for (const v of poly.vertices) {
2599
+ if (v[2] < minWorldZ) minWorldZ = v[2];
2600
+ }
2601
+ }
2602
+ }
2603
+ if (!Number.isFinite(minWorldZ)) {
2604
+ el.style.removeProperty("--shadow-ground-cssz");
2605
+ return;
2606
+ }
2607
+ const lift = shadow?.lift ?? 0.05;
2608
+ const groundCssZ = (minWorldZ + lift) * import_polycss_core4.BASE_TILE;
2609
+ el.style.setProperty("--shadow-ground-cssz", groundCssZ.toFixed(3));
2610
+ }, [sceneElRef, textureLighting, shadow]);
2611
+ (0, import_react9.useEffect)(() => {
2612
+ const el = sceneElRef.current;
2613
+ if (!el) return;
2614
+ if (textureLighting !== "dynamic") {
2615
+ el.style.removeProperty("--shadow-ground-cssz");
2616
+ }
2617
+ }, [textureLighting, sceneElRef]);
2618
+ const disabledStrategies = (0, import_react9.useMemo)(
2619
+ () => strategies?.disable?.length ? new Set(strategies.disable) : void 0,
2620
+ [strategies]
2621
+ );
1705
2622
  const polyChildren = textureAtlas.entries.map((entry, index) => {
1706
2623
  if (entry) {
1707
2624
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -1716,13 +2633,21 @@ function PolySceneInner({
1716
2633
  }
1717
2634
  const plan = textureAtlasPlans[index];
1718
2635
  if (!plan || plan.texture) return null;
1719
- return isSolidTrianglePlan(plan) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TextureTrianglePoly, { entry: plan, textureLighting }, plan.index) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TextureBorderShapePoly, { entry: plan }, plan.index);
2636
+ const useU = !disabledStrategies?.has("u");
2637
+ const useProjectiveSolid = !disabledStrategies?.has("b");
2638
+ if (useU && isSolidTrianglePlan(plan)) {
2639
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TextureTrianglePoly, { entry: plan, textureLighting }, plan.index);
2640
+ }
2641
+ if (useProjectiveSolid && isProjectiveQuadPlan(plan)) {
2642
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TextureProjectiveSolidPoly, { entry: plan, textureLighting }, plan.index);
2643
+ }
2644
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TextureBorderShapePoly, { entry: plan, disabledStrategies }, plan.index);
1720
2645
  });
1721
2646
  const sceneCtxValue = (0, import_react9.useMemo)(
1722
- () => ({ textureLighting, directionalLight, ambientLight }),
1723
- [textureLighting, directionalLight, ambientLight]
2647
+ () => ({ textureLighting, directionalLight, ambientLight, shadow, registerShadowCaster }),
2648
+ [textureLighting, directionalLight, ambientLight, shadow, registerShadowCaster]
1724
2649
  );
1725
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2650
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1726
2651
  "div",
1727
2652
  {
1728
2653
  ref: localSceneRef,
@@ -1736,13 +2661,10 @@ function PolySceneInner({
1736
2661
  // No more --polycss-rows / --polycss-cols — CSS Grid was dropped
1737
2662
  // in Phase 4 (per §Design.4a).
1738
2663
  },
1739
- children: autoCenterTransform ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "polycss-offset", style: { ["--offset-transform"]: autoCenterTransform }, children: [
1740
- polyChildren,
1741
- children
1742
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2664
+ children: [
1743
2665
  polyChildren,
1744
2666
  children
1745
- ] })
2667
+ ]
1746
2668
  }
1747
2669
  ) });
1748
2670
  }
@@ -1881,11 +2803,17 @@ function findMeshUnderPoint(clientX, clientY, filter) {
1881
2803
  }
1882
2804
 
1883
2805
  // src/scene/PolyMesh.tsx
1884
- var import_jsx_runtime5 = (
1885
- // Render-prop: caller controls how each polygon renders. We still
1886
- // wrap in a fragment with key so React reconciliation works.
1887
- require("react/jsx-runtime")
1888
- );
2806
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2807
+ function solidPaintVars(defaults) {
2808
+ const out = {};
2809
+ if (defaults.paintColor) out["--polycss-paint"] = defaults.paintColor;
2810
+ if (defaults.dynamicColor) {
2811
+ out["--psr"] = (defaults.dynamicColor.r / 255).toFixed(4);
2812
+ out["--psg"] = (defaults.dynamicColor.g / 255).toFixed(4);
2813
+ out["--psb"] = (defaults.dynamicColor.b / 255).toFixed(4);
2814
+ }
2815
+ return Object.keys(out).length > 0 ? out : null;
2816
+ }
1889
2817
  function buildTransform(position, scale, rotation) {
1890
2818
  const parts = [];
1891
2819
  if (position) {
@@ -1931,7 +2859,8 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
1931
2859
  polygons: polygonsProp,
1932
2860
  autoCenter,
1933
2861
  textureLighting,
1934
- atlasScale,
2862
+ textureQuality,
2863
+ castShadow,
1935
2864
  children,
1936
2865
  fallback,
1937
2866
  errorFallback,
@@ -1959,12 +2888,43 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
1959
2888
  return { ...parseOptions ?? {}, ...mtl ? { mtlUrl: mtl } : {} };
1960
2889
  }, [mtl, parseOptions]);
1961
2890
  const fetched = usePolyMesh(src ?? "", mergedOptions);
1962
- const sourcePolygons = src ? fetched.polygons : polygonsProp ?? [];
2891
+ const externalPolygons = src ? fetched.polygons : polygonsProp ?? [];
2892
+ const [localPolygons, setLocalPolygons] = (0, import_react11.useState)(null);
2893
+ const prevExternalRef = (0, import_react11.useRef)(externalPolygons);
2894
+ if (prevExternalRef.current !== externalPolygons) {
2895
+ prevExternalRef.current = externalPolygons;
2896
+ if (localPolygons !== null) setLocalPolygons(null);
2897
+ }
2898
+ const sourcePolygons = localPolygons ?? externalPolygons;
2899
+ const hasRenderProp = typeof children === "function";
2900
+ const renderPolygon = hasRenderProp ? children : null;
2901
+ const staticChildren = hasRenderProp ? null : children;
1963
2902
  const polygons = (0, import_react11.useMemo)(
1964
2903
  () => autoCenter ? recenterPolygons(sourcePolygons) : sourcePolygons,
1965
2904
  [sourcePolygons, autoCenter]
1966
2905
  );
1967
2906
  const transform = buildTransform(position, scale, rotation);
2907
+ const transformOrigin = (0, import_react11.useMemo)(() => {
2908
+ if (polygons.length === 0) return void 0;
2909
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
2910
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
2911
+ for (const poly of polygons) {
2912
+ for (const v of poly.vertices) {
2913
+ if (v[0] < minX) minX = v[0];
2914
+ if (v[0] > maxX) maxX = v[0];
2915
+ if (v[1] < minY) minY = v[1];
2916
+ if (v[1] > maxY) maxY = v[1];
2917
+ if (v[2] < minZ) minZ = v[2];
2918
+ if (v[2] > maxZ) maxZ = v[2];
2919
+ }
2920
+ }
2921
+ if (!Number.isFinite(minX)) return void 0;
2922
+ const tile = 50;
2923
+ const x = (minY + maxY) / 2 * tile;
2924
+ const y = (minX + maxX) / 2 * tile;
2925
+ const z = (minZ + maxZ) / 2 * tile;
2926
+ return `${x}px ${y}px ${z}px`;
2927
+ }, [polygons]);
1968
2928
  const wrapperRef = (0, import_react11.useRef)(null);
1969
2929
  const propsRef = (0, import_react11.useRef)({ position, scale, rotation });
1970
2930
  propsRef.current = { position, scale, rotation };
@@ -1980,7 +2940,14 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
1980
2940
  getRotation: () => propsRef.current.rotation,
1981
2941
  getScale: () => propsRef.current.scale,
1982
2942
  getPolygons: () => polygonsRef.current,
1983
- rebakeAtlas: () => setBakedRotation(propsRef.current.rotation)
2943
+ rebakeAtlas: () => setBakedRotation(propsRef.current.rotation),
2944
+ updatePolygon(target, partial) {
2945
+ const current = polygonsRef.current;
2946
+ const idx = typeof target === "number" ? target : current.indexOf(target);
2947
+ if (idx < 0 || idx >= current.length) return;
2948
+ Object.assign(current[idx], partial);
2949
+ setLocalPolygons([...current]);
2950
+ }
1984
2951
  }), [id]);
1985
2952
  (0, import_react11.useImperativeHandle)(forwardedRef, () => handle, [handle]);
1986
2953
  (0, import_react11.useEffect)(() => {
@@ -2130,11 +3097,6 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
2130
3097
  ["--plz"]: (localDir[2] / len).toFixed(4)
2131
3098
  };
2132
3099
  }, [effectiveTextureLighting, rotation, sceneDirectionalLight]);
2133
- const wrapperStyle = {
2134
- transform,
2135
- ...dynamicLightOverride,
2136
- ...style
2137
- };
2138
3100
  const bakedDirectional = (0, import_react11.useMemo)(() => {
2139
3101
  if (!effectiveDirectional) return effectiveDirectional;
2140
3102
  const rot = bakedRotation ?? [0, 0, 0];
@@ -2145,18 +3107,85 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
2145
3107
  };
2146
3108
  }, [effectiveDirectional, bakedRotation]);
2147
3109
  const atlasPlans = (0, import_react11.useMemo)(
2148
- () => !children ? polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
2149
- directionalLight: bakedDirectional,
2150
- ambientLight: effectiveAmbient
2151
- })) : [],
2152
- [children, polygons, bakedDirectional, effectiveAmbient]
3110
+ () => {
3111
+ if (renderPolygon) return [];
3112
+ const repairEdges = buildTextureEdgeRepairSets(polygons);
3113
+ return polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
3114
+ directionalLight: bakedDirectional,
3115
+ ambientLight: effectiveAmbient,
3116
+ textureEdgeRepairEdges: repairEdges[i]
3117
+ }));
3118
+ },
3119
+ [renderPolygon, polygons, bakedDirectional, effectiveAmbient]
2153
3120
  );
2154
3121
  const textureAtlas = useTextureAtlas(
2155
3122
  atlasPlans,
2156
3123
  effectiveTextureLighting,
2157
- atlasScale
3124
+ textureQuality
2158
3125
  );
2159
- const renderedPolygons = children ? polygons.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RenderPropPolygon, { polygon: p, index: i, children }, i)) : textureAtlas.entries.map((entry, index) => {
3126
+ const solidPaintDefaults = (0, import_react11.useMemo)(
3127
+ () => !renderPolygon ? getSolidPaintDefaults(atlasPlans, effectiveTextureLighting) : {},
3128
+ [renderPolygon, atlasPlans, effectiveTextureLighting]
3129
+ );
3130
+ const defaultPaintVars = (0, import_react11.useMemo)(
3131
+ () => solidPaintVars(solidPaintDefaults),
3132
+ [solidPaintDefaults]
3133
+ );
3134
+ const meshIdRef = (0, import_react11.useRef)(/* @__PURE__ */ Symbol());
3135
+ const sceneRegisterShadowCaster = sceneCtx?.registerShadowCaster;
3136
+ (0, import_react11.useEffect)(() => {
3137
+ if (!sceneRegisterShadowCaster) return;
3138
+ if (castShadow && effectiveTextureLighting === "dynamic") {
3139
+ sceneRegisterShadowCaster(meshIdRef.current, polygons);
3140
+ } else {
3141
+ sceneRegisterShadowCaster(meshIdRef.current, null);
3142
+ }
3143
+ return () => {
3144
+ sceneRegisterShadowCaster(meshIdRef.current, null);
3145
+ };
3146
+ }, [sceneRegisterShadowCaster, castShadow, effectiveTextureLighting, polygons]);
3147
+ const shadowLeaves = (0, import_react11.useMemo)(() => {
3148
+ if (!castShadow || effectiveTextureLighting !== "dynamic" || renderPolygon) return [];
3149
+ const shadowColor = sceneCtx?.shadow?.color ?? "#000000";
3150
+ const shadowOpacity = sceneCtx?.shadow?.opacity ?? 0.25;
3151
+ const parsed = (0, import_polycss_core6.parseHexColor)(shadowColor)?.rgb ?? [0, 0, 0];
3152
+ const shadowColorCss = `rgba(${parsed[0]},${parsed[1]},${parsed[2]},${shadowOpacity})`;
3153
+ const shadowDedupDrop = (0, import_polycss_core6.findOverlappingPolygonDuplicates)(polygons, {
3154
+ normalTolerance: 0.1,
3155
+ distanceTolerance: 0.5,
3156
+ overlapFraction: 0.4
3157
+ });
3158
+ const leaves = [];
3159
+ for (const plan of atlasPlans) {
3160
+ if (!plan) continue;
3161
+ if (shadowDedupDrop.has(plan.index)) continue;
3162
+ const borderShape = cssBorderShapeForPlan(plan);
3163
+ leaves.push(
3164
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3165
+ ShadowLeaf,
3166
+ {
3167
+ plan,
3168
+ shadowColorCss,
3169
+ borderShape
3170
+ },
3171
+ `shadow-${plan.index}`
3172
+ )
3173
+ );
3174
+ }
3175
+ return leaves;
3176
+ }, [castShadow, effectiveTextureLighting, renderPolygon, polygons, atlasPlans, sceneCtx?.shadow]);
3177
+ const wrapperStyle = {
3178
+ transform,
3179
+ ...transformOrigin ? { transformOrigin } : null,
3180
+ ...dynamicLightOverride,
3181
+ ...style,
3182
+ ...defaultPaintVars
3183
+ };
3184
+ const renderedPolygons = renderPolygon ? polygons.map((p, i) => (
3185
+ // Render-prop: caller controls how each polygon renders. We still
3186
+ // wrap in a fragment with key so React reconciliation works.
3187
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RenderPropPolygon, { polygon: p, index: i, children: renderPolygon }, i)
3188
+ )) : textureAtlas.entries.map((entry, index) => {
2160
3189
  if (entry) {
2161
3190
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2162
3191
  TextureAtlasPoly,
@@ -2170,7 +3199,22 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
2170
3199
  }
2171
3200
  const plan = atlasPlans[index];
2172
3201
  if (!plan || plan.texture) return null;
2173
- return isSolidTrianglePlan(plan) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TextureTrianglePoly, { entry: plan, textureLighting: effectiveTextureLighting }, plan.index) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TextureBorderShapePoly, { entry: plan }, plan.index);
3202
+ return isSolidTrianglePlan(plan) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3203
+ TextureTrianglePoly,
3204
+ {
3205
+ entry: plan,
3206
+ textureLighting: effectiveTextureLighting,
3207
+ solidPaintDefaults
3208
+ },
3209
+ plan.index
3210
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3211
+ TextureBorderShapePoly,
3212
+ {
3213
+ entry: plan,
3214
+ solidPaintDefaults
3215
+ },
3216
+ plan.index
3217
+ );
2174
3218
  });
2175
3219
  if (src) {
2176
3220
  if (fetched.loading && fetched.polygons.length === 0) {
@@ -2200,7 +3244,7 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
2200
3244
  );
2201
3245
  }
2202
3246
  }
2203
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3247
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2204
3248
  "div",
2205
3249
  {
2206
3250
  ref: wrapperRef,
@@ -2208,7 +3252,11 @@ var PolyMesh = (0, import_react11.forwardRef)(function PolyMesh2({
2208
3252
  className: `polycss-mesh${className ? ` ${className}` : ""}`,
2209
3253
  style: wrapperStyle,
2210
3254
  ...wrapperHandlers,
2211
- children: renderedPolygons
3255
+ children: [
3256
+ shadowLeaves,
3257
+ renderedPolygons,
3258
+ staticChildren
3259
+ ]
2212
3260
  }
2213
3261
  );
2214
3262
  });
@@ -2219,11 +3267,68 @@ function RenderPropPolygon({
2219
3267
  }) {
2220
3268
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children(polygon, index) });
2221
3269
  }
3270
+ function ShadowLeaf({
3271
+ plan,
3272
+ shadowColorCss,
3273
+ borderShape
3274
+ }) {
3275
+ const setRef = (0, import_react11.useCallback)((el) => {
3276
+ if (!el) return;
3277
+ el.style.setProperty("border-shape", borderShape);
3278
+ }, [borderShape]);
3279
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3280
+ "q",
3281
+ {
3282
+ ref: setRef,
3283
+ className: "polycss-shadow",
3284
+ style: {
3285
+ transform: `var(--shadow-proj) matrix3d(${plan.matrix})`,
3286
+ color: shadowColorCss,
3287
+ width: plan.canvasW,
3288
+ height: plan.canvasH,
3289
+ ["--pnx"]: plan.normal[0].toFixed(4),
3290
+ ["--pny"]: plan.normal[1].toFixed(4),
3291
+ ["--pnz"]: plan.normal[2].toFixed(4)
3292
+ }
3293
+ }
3294
+ );
3295
+ }
2222
3296
 
2223
- // src/scene/usePolyMaterial.ts
3297
+ // src/scene/PolyGround.tsx
2224
3298
  var import_react12 = require("react");
3299
+ var import_jsx_runtime6 = require("react/jsx-runtime");
3300
+ function PolyGround({
3301
+ size = 6,
3302
+ z = 0,
3303
+ center = [0, 0],
3304
+ color = "#7d848e",
3305
+ className
3306
+ }) {
3307
+ const polygons = (0, import_react12.useMemo)(() => {
3308
+ const half = size / 2;
3309
+ const [cx, cy] = center;
3310
+ const vertices = [
3311
+ [cx - half, cy - half, z],
3312
+ [cx + half, cy - half, z],
3313
+ [cx + half, cy + half, z],
3314
+ [cx - half, cy + half, z]
3315
+ ];
3316
+ return [{ vertices, color }];
3317
+ }, [size, z, center, color]);
3318
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3319
+ PolyMesh,
3320
+ {
3321
+ polygons,
3322
+ castShadow: false,
3323
+ className: className ? `polycss-ground ${className}` : "polycss-ground"
3324
+ }
3325
+ );
3326
+ }
3327
+
3328
+ // src/scene/usePolyMaterial.ts
3329
+ var import_react13 = require("react");
2225
3330
  function usePolyMaterial(options) {
2226
- return (0, import_react12.useMemo)(
3331
+ return (0, import_react13.useMemo)(
2227
3332
  () => ({ texture: options.texture, key: options.key }),
2228
3333
  // eslint-disable-next-line react-hooks/exhaustive-deps
2229
3334
  [options.texture, options.key]
@@ -2231,8 +3336,13 @@ function usePolyMaterial(options) {
2231
3336
  }
2232
3337
 
2233
3338
  // src/shapes/Poly.tsx
2234
- var import_react13 = require("react");
2235
- var import_jsx_runtime6 = require("react/jsx-runtime");
3339
+ var import_react14 = require("react");
3340
+ var import_jsx_runtime7 = require("react/jsx-runtime");
3341
+ var DIRECT_TEXTURE_CSS_DECIMALS = 4;
3342
+ function formatCssLength2(value, decimals = DIRECT_TEXTURE_CSS_DECIMALS) {
3343
+ const next = value.toFixed(decimals).replace(/\.?0+$/, "");
3344
+ return Number(next) === 0 || Object.is(Number(next), -0) ? "0" : `${next}px`;
3345
+ }
2236
3346
  function isAxisAlignedRectUVs(uvs) {
2237
3347
  if (uvs.length !== 4) return null;
2238
3348
  const us = [...new Set(uvs.map((uv) => uv[0]))].sort((a, b) => a - b);
@@ -2262,18 +3372,16 @@ function MaterialDirectPoly({
2262
3372
  const { u0, u1, v0, v1 } = uvRect;
2263
3373
  const du = u1 - u0;
2264
3374
  const dv = v1 - v0;
2265
- const sourceW = plan.canvasW / du;
2266
- const sourceH = plan.canvasH / dv;
3375
+ const sourceW = 1 / du;
3376
+ const sourceH = 1 / dv;
2267
3377
  const vMax = Math.max(v0, v1);
2268
- const offsetX = u0 * sourceW;
2269
- const offsetY = (1 - vMax) * sourceH;
3378
+ const offsetX = u0 / du;
3379
+ const offsetY = (1 - vMax) / dv;
2270
3380
  const style = {
2271
- width: plan.canvasW,
2272
- height: plan.canvasH,
2273
- transform: `matrix3d(${plan.matrix})`,
3381
+ transform: `matrix3d(${plan.canonicalMatrix})`,
2274
3382
  backgroundImage: `url(${material.texture})`,
2275
- backgroundSize: `${sourceW}px ${sourceH}px`,
2276
- backgroundPosition: `-${offsetX}px -${offsetY}px`,
3383
+ backgroundSize: `${formatCssLength2(sourceW)} ${formatCssLength2(sourceH)}`,
3384
+ backgroundPosition: `${formatCssLength2(-offsetX)} ${formatCssLength2(-offsetY)}`,
2277
3385
  pointerEvents: pointerEvents === "none" ? "none" : void 0,
2278
3386
  ...styleProp
2279
3387
  };
@@ -2281,7 +3389,7 @@ function MaterialDirectPoly({
2281
3389
  Object.entries(plan.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
2282
3390
  ) : {};
2283
3391
  const elementClassName = className?.trim() || void 0;
2284
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3392
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2285
3393
  "i",
2286
3394
  {
2287
3395
  className: elementClassName,
@@ -2324,17 +3432,17 @@ function PolyInner({
2324
3432
  pointerEvents: pointerEventsProp,
2325
3433
  context,
2326
3434
  textureLighting: textureLightingProp,
2327
- atlasScale: atlasScaleProp,
3435
+ textureQuality: textureQualityProp,
2328
3436
  baseColor: baseColorProp,
2329
3437
  ...dataAttrs
2330
3438
  }) {
2331
3439
  const tileSize = context?.tileSize ?? 50;
2332
3440
  const layerElevation = context?.layerElevation ?? tileSize;
2333
3441
  const textureLighting = textureLightingProp ?? context?.textureLighting ?? "baked";
2334
- const atlasScale = atlasScaleProp ?? context?.atlasScale;
3442
+ const textureQuality = textureQualityProp ?? context?.textureQuality;
2335
3443
  const polygonColor = baseColorProp ?? color;
2336
3444
  const effectiveTexture = material?.texture ?? texture;
2337
- const atlasPlan = (0, import_react13.useMemo)(
3445
+ const atlasPlan = (0, import_react14.useMemo)(
2338
3446
  () => computeTextureAtlasPlan(
2339
3447
  { vertices, color: polygonColor, texture: effectiveTexture, uvs, data },
2340
3448
  0,
@@ -2355,15 +3463,15 @@ function PolyInner({
2355
3463
  context?.directionalLight
2356
3464
  ]
2357
3465
  );
2358
- const materialUvRect = (0, import_react13.useMemo)(
3466
+ const materialUvRect = (0, import_react14.useMemo)(
2359
3467
  () => material && uvs ? isAxisAlignedRectUVs(uvs) : null,
2360
3468
  [material, uvs]
2361
3469
  );
2362
- const atlasPlans = (0, import_react13.useMemo)(
3470
+ const atlasPlans = (0, import_react14.useMemo)(
2363
3471
  () => materialUvRect ? [] : [atlasPlan],
2364
3472
  [materialUvRect, atlasPlan]
2365
3473
  );
2366
- const textureAtlas = useTextureAtlas(atlasPlans, textureLighting, atlasScale);
3474
+ const textureAtlas = useTextureAtlas(atlasPlans, textureLighting, textureQuality);
2367
3475
  const domEventHandlers = {
2368
3476
  onClick,
2369
3477
  onDoubleClick,
@@ -2412,7 +3520,7 @@ function PolyInner({
2412
3520
  const wrapperTransform = transformParts.length > 0 ? transformParts.join(" ") : void 0;
2413
3521
  let front = null;
2414
3522
  if (materialUvRect && material && atlasPlan) {
2415
- front = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3523
+ front = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2416
3524
  MaterialDirectPoly,
2417
3525
  {
2418
3526
  plan: atlasPlan,
@@ -2428,7 +3536,7 @@ function PolyInner({
2428
3536
  } else {
2429
3537
  const atlasEntry = textureAtlas.entries[0];
2430
3538
  if (atlasEntry) {
2431
- front = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3539
+ front = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2432
3540
  TextureAtlasPoly,
2433
3541
  {
2434
3542
  entry: atlasEntry,
@@ -2442,7 +3550,7 @@ function PolyInner({
2442
3550
  }
2443
3551
  );
2444
3552
  } else if (atlasPlan && !atlasPlan.texture) {
2445
- front = isSolidTrianglePlan(atlasPlan) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3553
+ front = isSolidTrianglePlan(atlasPlan) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2446
3554
  TextureTrianglePoly,
2447
3555
  {
2448
3556
  entry: atlasPlan,
@@ -2453,7 +3561,18 @@ function PolyInner({
2453
3561
  domEventHandlers,
2454
3562
  pointerEvents: pointerEventsProp ?? "auto"
2455
3563
  }
2456
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3564
+ ) : isProjectiveQuadPlan(atlasPlan) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3565
+ TextureProjectiveSolidPoly,
3566
+ {
3567
+ entry: atlasPlan,
3568
+ textureLighting,
3569
+ className,
3570
+ style: styleProp,
3571
+ domAttrs,
3572
+ domEventHandlers,
3573
+ pointerEvents: pointerEventsProp ?? "auto"
3574
+ }
3575
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2457
3576
  TextureBorderShapePoly,
2458
3577
  {
2459
3578
  entry: atlasPlan,
@@ -2468,26 +3587,441 @@ function PolyInner({
2468
3587
  }
2469
3588
  if (!front) return null;
2470
3589
  if (!wrapperTransform) return front;
2471
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3590
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2472
3591
  "div",
2473
3592
  {
2474
3593
  className: "polycss-poly-wrapper",
2475
3594
  style: {
3595
+ transform: wrapperTransform,
2476
3596
  position: "absolute",
2477
- transformStyle: "preserve-3d",
2478
- transform: wrapperTransform
3597
+ transformStyle: "preserve-3d"
2479
3598
  },
2480
3599
  children: front
2481
3600
  }
2482
- );
2483
- }
2484
- var Poly = (0, import_react13.memo)(PolyInner);
3601
+ );
3602
+ }
3603
+ var Poly = (0, import_react14.memo)(PolyInner);
3604
+
3605
+ // src/controls/PolyFirstPersonControls.tsx
3606
+ var import_react15 = require("react");
3607
+ var import_polycss_core7 = require("@layoutit/polycss-core");
3608
+ var DEFAULTS = {
3609
+ enabled: true,
3610
+ lookEnabled: true,
3611
+ moveEnabled: true,
3612
+ jumpEnabled: true,
3613
+ crouchEnabled: true,
3614
+ lookSensitivity: 0.15,
3615
+ invertY: false,
3616
+ moveSpeed: 5,
3617
+ jumpVelocity: 7,
3618
+ gravity: 18,
3619
+ eyeHeight: 1.7,
3620
+ crouchHeight: 1,
3621
+ groundZ: 0,
3622
+ minPitch: 5,
3623
+ maxPitch: 175
3624
+ };
3625
+ function resolveOptions(base, partial) {
3626
+ return {
3627
+ enabled: partial.enabled ?? base.enabled,
3628
+ lookEnabled: partial.lookEnabled ?? base.lookEnabled,
3629
+ moveEnabled: partial.moveEnabled ?? base.moveEnabled,
3630
+ jumpEnabled: partial.jumpEnabled ?? base.jumpEnabled,
3631
+ crouchEnabled: partial.crouchEnabled ?? base.crouchEnabled,
3632
+ lookSensitivity: partial.lookSensitivity ?? base.lookSensitivity,
3633
+ invertY: partial.invertY ?? base.invertY,
3634
+ moveSpeed: partial.moveSpeed ?? base.moveSpeed,
3635
+ jumpVelocity: partial.jumpVelocity ?? base.jumpVelocity,
3636
+ gravity: partial.gravity ?? base.gravity,
3637
+ eyeHeight: partial.eyeHeight ?? base.eyeHeight,
3638
+ crouchHeight: partial.crouchHeight ?? base.crouchHeight,
3639
+ groundZ: partial.groundZ ?? base.groundZ,
3640
+ minPitch: partial.minPitch ?? base.minPitch,
3641
+ maxPitch: partial.maxPitch ?? base.maxPitch
3642
+ };
3643
+ }
3644
+ var FORWARD_KEYS = /* @__PURE__ */ new Set(["KeyW", "ArrowUp"]);
3645
+ var BACK_KEYS = /* @__PURE__ */ new Set(["KeyS", "ArrowDown"]);
3646
+ var LEFT_KEYS = /* @__PURE__ */ new Set(["KeyA", "ArrowLeft"]);
3647
+ var RIGHT_KEYS = /* @__PURE__ */ new Set(["KeyD", "ArrowRight"]);
3648
+ var JUMP_KEYS = /* @__PURE__ */ new Set(["Space"]);
3649
+ var CROUCH_KEYS = /* @__PURE__ */ new Set(["ControlLeft", "ControlRight"]);
3650
+ function isFpvKey(code) {
3651
+ 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);
3652
+ }
3653
+ function makeRegistry() {
3654
+ return { change: [], start: [], end: [] };
3655
+ }
3656
+ function emitEvent(registry, type) {
3657
+ const list = [...registry[type]];
3658
+ for (const fn of list) {
3659
+ try {
3660
+ fn();
3661
+ } catch {
3662
+ }
3663
+ }
3664
+ }
3665
+ var PolyFirstPersonControls = (0, import_react15.forwardRef)(function PolyFirstPersonControls2(props, ref) {
3666
+ const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
3667
+ const onChangeRef = (0, import_react15.useRef)(props.onChange);
3668
+ const onInteractionStartRef = (0, import_react15.useRef)(props.onInteractionStart);
3669
+ const onInteractionEndRef = (0, import_react15.useRef)(props.onInteractionEnd);
3670
+ (0, import_react15.useEffect)(() => {
3671
+ onChangeRef.current = props.onChange;
3672
+ onInteractionStartRef.current = props.onInteractionStart;
3673
+ onInteractionEndRef.current = props.onInteractionEnd;
3674
+ });
3675
+ const optsRef = (0, import_react15.useRef)(resolveOptions(DEFAULTS, props));
3676
+ const cameraOriginRef = (0, import_react15.useRef)([0, 0, 0]);
3677
+ const rafIdRef = (0, import_react15.useRef)(null);
3678
+ const lastTimeRef = (0, import_react15.useRef)(0);
3679
+ const stoppedRef = (0, import_react15.useRef)(false);
3680
+ const pointerLockedRef = (0, import_react15.useRef)(false);
3681
+ const interactingRef = (0, import_react15.useRef)(false);
3682
+ const keysHeldRef = (0, import_react15.useRef)(/* @__PURE__ */ new Set());
3683
+ const verticalVelRef = (0, import_react15.useRef)(0);
3684
+ const jumpOffsetRef = (0, import_react15.useRef)(0);
3685
+ const registryRef = (0, import_react15.useRef)(makeRegistry());
3686
+ (0, import_react15.useImperativeHandle)(ref, () => ({
3687
+ update(partial) {
3688
+ const prev = optsRef.current;
3689
+ optsRef.current = resolveOptions(prev, partial);
3690
+ if (!stoppedRef.current) {
3691
+ const host = cameraElRef.current;
3692
+ if (host) host.style.cursor = optsRef.current.lookEnabled ? "crosshair" : "";
3693
+ }
3694
+ },
3695
+ resume() {
3696
+ if (!stoppedRef.current) return;
3697
+ stoppedRef.current = false;
3698
+ const host = cameraElRef.current;
3699
+ if (host) host.style.cursor = optsRef.current.lookEnabled ? "crosshair" : "";
3700
+ startLoop();
3701
+ },
3702
+ pause() {
3703
+ if (stoppedRef.current) return;
3704
+ stoppedRef.current = true;
3705
+ stopLoop();
3706
+ const host = cameraElRef.current;
3707
+ if (host) host.style.cursor = "";
3708
+ if (interactingRef.current) {
3709
+ interactingRef.current = false;
3710
+ emitEvent(registryRef.current, "end");
3711
+ try {
3712
+ onInteractionEndRef.current?.();
3713
+ } catch {
3714
+ }
3715
+ }
3716
+ },
3717
+ destroy() {
3718
+ stoppedRef.current = true;
3719
+ stopLoop();
3720
+ },
3721
+ lock() {
3722
+ const opts = optsRef.current;
3723
+ if (!opts.enabled || !opts.lookEnabled || stoppedRef.current) return;
3724
+ const host = cameraElRef.current;
3725
+ try {
3726
+ host?.requestPointerLock();
3727
+ } catch {
3728
+ }
3729
+ },
3730
+ unlock() {
3731
+ if (pointerLockedRef.current) {
3732
+ const host = cameraElRef.current;
3733
+ try {
3734
+ host?.ownerDocument?.exitPointerLock();
3735
+ } catch {
3736
+ }
3737
+ }
3738
+ },
3739
+ isLocked() {
3740
+ return pointerLockedRef.current;
3741
+ },
3742
+ getOrigin() {
3743
+ const o = cameraOriginRef.current;
3744
+ return [o[0], o[1], o[2]];
3745
+ },
3746
+ setOrigin(origin) {
3747
+ cameraOriginRef.current[0] = origin[0];
3748
+ cameraOriginRef.current[1] = origin[1];
3749
+ cameraOriginRef.current[2] = origin[2];
3750
+ syncTargetFromOrigin();
3751
+ emitEvent(registryRef.current, "change");
3752
+ try {
3753
+ onChangeRef.current?.();
3754
+ } catch {
3755
+ }
3756
+ },
3757
+ addEventListener(type, listener) {
3758
+ const arr = registryRef.current[type];
3759
+ if (!arr.includes(listener)) arr.push(listener);
3760
+ },
3761
+ removeEventListener(type, listener) {
3762
+ const arr = registryRef.current[type];
3763
+ const idx = arr.indexOf(listener);
3764
+ if (idx >= 0) arr.splice(idx, 1);
3765
+ },
3766
+ hasEventListener(type, listener) {
3767
+ return registryRef.current[type].includes(listener);
3768
+ }
3769
+ }));
3770
+ function forwardDir(rotX, rotY) {
3771
+ const rx = rotX * Math.PI / 180;
3772
+ const ry = rotY * Math.PI / 180;
3773
+ return [
3774
+ -Math.sin(rx) * Math.cos(ry),
3775
+ -Math.sin(rx) * Math.sin(ry),
3776
+ -Math.cos(rx)
3777
+ ];
3778
+ }
3779
+ function lookOffset() {
3780
+ const host = cameraElRef.current;
3781
+ const perspStr = host ? getComputedStyle(host).perspective : "";
3782
+ const n = parseFloat(perspStr);
3783
+ return (Number.isFinite(n) && n > 0 ? n : 8e3) / import_polycss_core7.BASE_TILE;
3784
+ }
3785
+ function deriveTarget() {
3786
+ const s = cameraRef.current.state;
3787
+ const f = forwardDir(s.rotX ?? 90, s.rotY ?? 0);
3788
+ const d = lookOffset();
3789
+ const o = cameraOriginRef.current;
3790
+ return [o[0] + f[0] * d, o[1] + f[1] * d, o[2] + f[2] * d];
3791
+ }
3792
+ function syncTargetFromOrigin() {
3793
+ const t = deriveTarget();
3794
+ const handle = cameraRef.current;
3795
+ handle.update({ target: t });
3796
+ applyTransformDirect();
3797
+ store.updateCameraFromRef(handle);
3798
+ }
3799
+ const ANIM_DT_CLAMP = 0.05;
3800
+ function tick(now) {
3801
+ if (rafIdRef.current === null || stoppedRef.current) return;
3802
+ const dt = Math.min(ANIM_DT_CLAMP, lastTimeRef.current ? (now - lastTimeRef.current) / 1e3 : 0.0167);
3803
+ lastTimeRef.current = now;
3804
+ const opts = optsRef.current;
3805
+ if (opts.enabled) {
3806
+ let dirty = false;
3807
+ const s = cameraRef.current.state;
3808
+ const o = cameraOriginRef.current;
3809
+ if (opts.moveEnabled) {
3810
+ let mf = 0, mr = 0;
3811
+ for (const code of keysHeldRef.current) {
3812
+ if (FORWARD_KEYS.has(code)) mf += 1;
3813
+ else if (BACK_KEYS.has(code)) mf -= 1;
3814
+ else if (RIGHT_KEYS.has(code)) mr += 1;
3815
+ else if (LEFT_KEYS.has(code)) mr -= 1;
3816
+ }
3817
+ if (mf !== 0 || mr !== 0) {
3818
+ const rotY = s.rotY ?? 0;
3819
+ const r = rotY * Math.PI / 180;
3820
+ const fx = -Math.cos(r), fy = -Math.sin(r);
3821
+ const rx = -Math.sin(r), ry = Math.cos(r);
3822
+ const len = Math.hypot(mf, mr) || 1;
3823
+ const step = opts.moveSpeed * dt;
3824
+ o[0] += (fx * mf + rx * mr) / len * step;
3825
+ o[1] += (fy * mf + ry * mr) / len * step;
3826
+ dirty = true;
3827
+ }
3828
+ }
3829
+ const crouched = opts.crouchEnabled && (keysHeldRef.current.has("ControlLeft") || keysHeldRef.current.has("ControlRight"));
3830
+ const baseHeight = crouched ? opts.crouchHeight : opts.eyeHeight;
3831
+ if (opts.jumpEnabled && (verticalVelRef.current !== 0 || jumpOffsetRef.current > 0)) {
3832
+ verticalVelRef.current -= opts.gravity * dt;
3833
+ jumpOffsetRef.current += verticalVelRef.current * dt;
3834
+ if (jumpOffsetRef.current <= 0) {
3835
+ jumpOffsetRef.current = 0;
3836
+ verticalVelRef.current = 0;
3837
+ }
3838
+ } else if (!opts.jumpEnabled) {
3839
+ jumpOffsetRef.current = 0;
3840
+ verticalVelRef.current = 0;
3841
+ }
3842
+ const originZ = opts.groundZ + baseHeight + jumpOffsetRef.current;
3843
+ if (Math.abs(o[2] - originZ) > 1e-4) {
3844
+ o[2] = originZ;
3845
+ dirty = true;
3846
+ }
3847
+ if (dirty) {
3848
+ const t = deriveTarget();
3849
+ const handle = cameraRef.current;
3850
+ handle.update({ target: t });
3851
+ applyTransformDirect();
3852
+ store.updateCameraFromRef(handle);
3853
+ emitEvent(registryRef.current, "change");
3854
+ try {
3855
+ onChangeRef.current?.();
3856
+ } catch {
3857
+ }
3858
+ }
3859
+ }
3860
+ rafIdRef.current = requestAnimationFrame(tick);
3861
+ }
3862
+ function startLoop() {
3863
+ if (rafIdRef.current !== null || stoppedRef.current) return;
3864
+ lastTimeRef.current = 0;
3865
+ rafIdRef.current = requestAnimationFrame(tick);
3866
+ }
3867
+ function stopLoop() {
3868
+ if (rafIdRef.current === null) return;
3869
+ cancelAnimationFrame(rafIdRef.current);
3870
+ rafIdRef.current = null;
3871
+ }
3872
+ (0, import_react15.useEffect)(() => {
3873
+ const host = cameraElRef.current;
3874
+ if (!host) return;
3875
+ const doc = host.ownerDocument ?? document;
3876
+ const win = doc.defaultView ?? globalThis;
3877
+ stoppedRef.current = false;
3878
+ pointerLockedRef.current = false;
3879
+ interactingRef.current = false;
3880
+ keysHeldRef.current.clear();
3881
+ verticalVelRef.current = 0;
3882
+ jumpOffsetRef.current = 0;
3883
+ const s = cameraRef.current.state;
3884
+ const t = s.target ?? [0, 0, 0];
3885
+ const opts = optsRef.current;
3886
+ cameraOriginRef.current = [t[0], t[1], opts.groundZ + opts.eyeHeight];
3887
+ syncTargetFromOrigin();
3888
+ host.style.cursor = opts.lookEnabled ? "crosshair" : "";
3889
+ const computedPersp = win.getComputedStyle(host).perspective;
3890
+ const persp = parseFloat(computedPersp);
3891
+ const effectivePersp = Number.isFinite(persp) && persp > 0 ? persp : 2e3;
3892
+ host.style.setProperty("--polycss-fpv-perspective", `${effectivePersp}px`);
3893
+ host.classList.add("polycss-fpv-host");
3894
+ const onHostClick = () => {
3895
+ const o = optsRef.current;
3896
+ if (!o.enabled || !o.lookEnabled || stoppedRef.current || pointerLockedRef.current) return;
3897
+ try {
3898
+ host.requestPointerLock();
3899
+ } catch {
3900
+ }
3901
+ };
3902
+ const onPointerLockChange = () => {
3903
+ const locked = doc.pointerLockElement === host;
3904
+ if (locked === pointerLockedRef.current) return;
3905
+ pointerLockedRef.current = locked;
3906
+ if (locked) {
3907
+ interactingRef.current = true;
3908
+ emitEvent(registryRef.current, "start");
3909
+ try {
3910
+ onInteractionStartRef.current?.();
3911
+ } catch {
3912
+ }
3913
+ } else {
3914
+ if (interactingRef.current) {
3915
+ interactingRef.current = false;
3916
+ emitEvent(registryRef.current, "end");
3917
+ try {
3918
+ onInteractionEndRef.current?.();
3919
+ } catch {
3920
+ }
3921
+ }
3922
+ }
3923
+ };
3924
+ const onMouseMove = (e) => {
3925
+ if (!pointerLockedRef.current || stoppedRef.current) return;
3926
+ const o = optsRef.current;
3927
+ if (!o.enabled || !o.lookEnabled) return;
3928
+ const dx = e.movementX ?? 0;
3929
+ const dy = e.movementY ?? 0;
3930
+ if (dx === 0 && dy === 0) return;
3931
+ const handle = cameraRef.current;
3932
+ const sceneOpts = handle.state;
3933
+ const sens = o.lookSensitivity;
3934
+ const dyDir = o.invertY ? -1 : 1;
3935
+ const rotY = (((sceneOpts.rotY ?? 0) - dx * sens) % 360 + 360) % 360;
3936
+ let rotX = (sceneOpts.rotX ?? 90) - dy * sens * dyDir;
3937
+ if (rotX < o.minPitch) rotX = o.minPitch;
3938
+ else if (rotX > o.maxPitch) rotX = o.maxPitch;
3939
+ const f = forwardDir(rotX, rotY);
3940
+ const d = lookOffset();
3941
+ const origin = cameraOriginRef.current;
3942
+ const target = [
3943
+ origin[0] + f[0] * d,
3944
+ origin[1] + f[1] * d,
3945
+ origin[2] + f[2] * d
3946
+ ];
3947
+ handle.update({ rotX, rotY, target });
3948
+ applyTransformDirect();
3949
+ store.updateCameraFromRef(handle);
3950
+ emitEvent(registryRef.current, "change");
3951
+ try {
3952
+ onChangeRef.current?.();
3953
+ } catch {
3954
+ }
3955
+ };
3956
+ const onKeyDown = (e) => {
3957
+ const o = optsRef.current;
3958
+ if (!o.enabled || stoppedRef.current) return;
3959
+ if (!isFpvKey(e.code)) return;
3960
+ if (!pointerLockedRef.current && !o.moveEnabled) return;
3961
+ if (JUMP_KEYS.has(e.code)) {
3962
+ if (!o.jumpEnabled) return;
3963
+ e.preventDefault();
3964
+ if (!keysHeldRef.current.has(e.code) && verticalVelRef.current === 0 && jumpOffsetRef.current === 0) {
3965
+ verticalVelRef.current = o.jumpVelocity;
3966
+ }
3967
+ keysHeldRef.current.add(e.code);
3968
+ return;
3969
+ }
3970
+ if (CROUCH_KEYS.has(e.code) && !o.crouchEnabled) return;
3971
+ if (!o.moveEnabled && !CROUCH_KEYS.has(e.code)) return;
3972
+ e.preventDefault();
3973
+ keysHeldRef.current.add(e.code);
3974
+ };
3975
+ const onKeyUp = (e) => {
3976
+ if (!isFpvKey(e.code)) return;
3977
+ keysHeldRef.current.delete(e.code);
3978
+ };
3979
+ const onBlur = () => {
3980
+ keysHeldRef.current.clear();
3981
+ };
3982
+ host.addEventListener("click", onHostClick);
3983
+ doc.addEventListener("pointerlockchange", onPointerLockChange);
3984
+ doc.addEventListener("mousemove", onMouseMove);
3985
+ win.addEventListener("keydown", onKeyDown);
3986
+ win.addEventListener("keyup", onKeyUp);
3987
+ win.addEventListener("blur", onBlur);
3988
+ startLoop();
3989
+ return () => {
3990
+ stoppedRef.current = true;
3991
+ stopLoop();
3992
+ host.removeEventListener("click", onHostClick);
3993
+ doc.removeEventListener("pointerlockchange", onPointerLockChange);
3994
+ doc.removeEventListener("mousemove", onMouseMove);
3995
+ win.removeEventListener("keydown", onKeyDown);
3996
+ win.removeEventListener("keyup", onKeyUp);
3997
+ win.removeEventListener("blur", onBlur);
3998
+ host.style.cursor = "";
3999
+ host.classList.remove("polycss-fpv-host");
4000
+ host.style.removeProperty("--polycss-fpv-perspective");
4001
+ keysHeldRef.current.clear();
4002
+ if (pointerLockedRef.current) {
4003
+ try {
4004
+ doc.exitPointerLock();
4005
+ } catch {
4006
+ }
4007
+ }
4008
+ };
4009
+ }, [cameraElRef, cameraRef, applyTransformDirect, store]);
4010
+ (0, import_react15.useEffect)(() => {
4011
+ optsRef.current = resolveOptions(optsRef.current, props);
4012
+ const host = cameraElRef.current;
4013
+ if (host && !stoppedRef.current) {
4014
+ host.style.cursor = optsRef.current.lookEnabled ? "crosshair" : "";
4015
+ }
4016
+ });
4017
+ return null;
4018
+ });
2485
4019
 
2486
- // src/controls/PolyOrbitControls.tsx
2487
- var import_react14 = require("react");
4020
+ // src/controls/PolyMapControls.tsx
4021
+ var import_react16 = require("react");
2488
4022
 
2489
4023
  // src/controls/sharedControls.ts
2490
- var import_polycss_core7 = require("@layoutit/polycss-core");
4024
+ var import_polycss_core8 = require("@layoutit/polycss-core");
2491
4025
  var POINTER_DRAG_SPEED = 4;
2492
4026
  function invertFactor(invert) {
2493
4027
  if (invert === true) return -1;
@@ -2508,7 +4042,7 @@ function applyPan(dx, dy, s, handle, _invert) {
2508
4042
  const cosRotX = cosRotXRaw >= 0 ? Math.max(0.1, cosRotXRaw) : Math.min(-0.1, cosRotXRaw);
2509
4043
  const cZ = Math.cos(s.rotY * Math.PI / 180);
2510
4044
  const sZ = Math.sin(s.rotY * Math.PI / 180);
2511
- const k = z * import_polycss_core7.BASE_TILE;
4045
+ const k = z * import_polycss_core8.BASE_TILE;
2512
4046
  const targetD0 = (dx * sZ - dy * cZ / cosRotX) / k;
2513
4047
  const targetD1 = -(dx * cZ + dy * sZ / cosRotX) / k;
2514
4048
  const t = s.target;
@@ -2641,8 +4175,8 @@ function makeAnimateEffect({
2641
4175
  };
2642
4176
  }
2643
4177
 
2644
- // src/controls/PolyOrbitControls.tsx
2645
- function PolyOrbitControls({
4178
+ // src/controls/PolyMapControls.tsx
4179
+ function PolyMapControls({
2646
4180
  drag = true,
2647
4181
  wheel = true,
2648
4182
  dolly = false,
@@ -2657,19 +4191,19 @@ function PolyOrbitControls({
2657
4191
  onInteractionEnd
2658
4192
  }) {
2659
4193
  const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
2660
- const dragRef = (0, import_react14.useRef)(drag);
2661
- const wheelRef = (0, import_react14.useRef)(wheel);
2662
- const dollyRef = (0, import_react14.useRef)(dolly);
2663
- const invertRef = (0, import_react14.useRef)(invert);
2664
- const zoomMinRef = (0, import_react14.useRef)(minZoom);
2665
- const zoomMaxRef = (0, import_react14.useRef)(maxZoom);
2666
- const distanceMinRef = (0, import_react14.useRef)(minDistance);
2667
- const distanceMaxRef = (0, import_react14.useRef)(maxDistance);
2668
- const animateRef = (0, import_react14.useRef)(animate);
2669
- const onChangeRef = (0, import_react14.useRef)(onChange);
2670
- const onInteractionStartRef = (0, import_react14.useRef)(onInteractionStart);
2671
- const onInteractionEndRef = (0, import_react14.useRef)(onInteractionEnd);
2672
- (0, import_react14.useEffect)(() => {
4194
+ const dragRef = (0, import_react16.useRef)(drag);
4195
+ const wheelRef = (0, import_react16.useRef)(wheel);
4196
+ const dollyRef = (0, import_react16.useRef)(dolly);
4197
+ const invertRef = (0, import_react16.useRef)(invert);
4198
+ const zoomMinRef = (0, import_react16.useRef)(minZoom);
4199
+ const zoomMaxRef = (0, import_react16.useRef)(maxZoom);
4200
+ const distanceMinRef = (0, import_react16.useRef)(minDistance);
4201
+ const distanceMaxRef = (0, import_react16.useRef)(maxDistance);
4202
+ const animateRef = (0, import_react16.useRef)(animate);
4203
+ const onChangeRef = (0, import_react16.useRef)(onChange);
4204
+ const onInteractionStartRef = (0, import_react16.useRef)(onInteractionStart);
4205
+ const onInteractionEndRef = (0, import_react16.useRef)(onInteractionEnd);
4206
+ (0, import_react16.useEffect)(() => {
2673
4207
  dragRef.current = drag;
2674
4208
  wheelRef.current = wheel;
2675
4209
  dollyRef.current = dolly;
@@ -2693,7 +4227,7 @@ function PolyOrbitControls({
2693
4227
  try {
2694
4228
  fn(cameraSnapshot());
2695
4229
  } catch (err) {
2696
- console.error("[polycss/react] PolyOrbitControls onChange threw:", err);
4230
+ console.error("[polycss/react] PolyMapControls onChange threw:", err);
2697
4231
  }
2698
4232
  };
2699
4233
  const fireStart = () => {
@@ -2702,7 +4236,7 @@ function PolyOrbitControls({
2702
4236
  try {
2703
4237
  fn(cameraSnapshot());
2704
4238
  } catch (err) {
2705
- console.error("[polycss/react] PolyOrbitControls onInteractionStart threw:", err);
4239
+ console.error("[polycss/react] PolyMapControls onInteractionStart threw:", err);
2706
4240
  }
2707
4241
  };
2708
4242
  const fireEnd = () => {
@@ -2711,11 +4245,11 @@ function PolyOrbitControls({
2711
4245
  try {
2712
4246
  fn(cameraSnapshot());
2713
4247
  } catch (err) {
2714
- console.error("[polycss/react] PolyOrbitControls onInteractionEnd threw:", err);
4248
+ console.error("[polycss/react] PolyMapControls onInteractionEnd threw:", err);
2715
4249
  }
2716
4250
  };
2717
- const animationPausedShared = (0, import_react14.useRef)({ value: false }).current;
2718
- (0, import_react14.useEffect)(() => {
4251
+ const animationPausedShared = (0, import_react16.useRef)({ value: false }).current;
4252
+ (0, import_react16.useEffect)(() => {
2719
4253
  if (!drag) return;
2720
4254
  const el = cameraElRef.current;
2721
4255
  if (!el) return;
@@ -2752,9 +4286,9 @@ function PolyOrbitControls({
2752
4286
  pointer = { x: e.clientX, y: e.clientY };
2753
4287
  const handle = cameraRef.current;
2754
4288
  if (e.shiftKey) {
2755
- buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2756
- } else {
2757
4289
  buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
4290
+ } else {
4291
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2758
4292
  }
2759
4293
  applyTransformDirect();
2760
4294
  store.updateCameraFromRef(handle);
@@ -2788,7 +4322,7 @@ function PolyOrbitControls({
2788
4322
  const dy = e.clientY - rightPointer.y;
2789
4323
  rightPointer = { x: e.clientX, y: e.clientY };
2790
4324
  const handle = cameraRef.current;
2791
- buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
4325
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2792
4326
  applyTransformDirect();
2793
4327
  store.updateCameraFromRef(handle);
2794
4328
  fireChange();
@@ -2825,12 +4359,12 @@ function PolyOrbitControls({
2825
4359
  el.style.userSelect = "";
2826
4360
  };
2827
4361
  }, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
2828
- (0, import_react14.useEffect)(
4362
+ (0, import_react16.useEffect)(
2829
4363
  () => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
2830
4364
  // eslint-disable-next-line react-hooks/exhaustive-deps
2831
4365
  [wheel, applyTransformDirect, cameraElRef, cameraRef, store]
2832
4366
  );
2833
- (0, import_react14.useEffect)(
4367
+ (0, import_react16.useEffect)(
2834
4368
  () => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
2835
4369
  // eslint-disable-next-line react-hooks/exhaustive-deps
2836
4370
  [!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
@@ -2838,9 +4372,9 @@ function PolyOrbitControls({
2838
4372
  return null;
2839
4373
  }
2840
4374
 
2841
- // src/controls/PolyMapControls.tsx
2842
- var import_react15 = require("react");
2843
- function PolyMapControls({
4375
+ // src/controls/PolyOrbitControls.tsx
4376
+ var import_react17 = require("react");
4377
+ function PolyOrbitControls({
2844
4378
  drag = true,
2845
4379
  wheel = true,
2846
4380
  dolly = false,
@@ -2855,19 +4389,19 @@ function PolyMapControls({
2855
4389
  onInteractionEnd
2856
4390
  }) {
2857
4391
  const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
2858
- const dragRef = (0, import_react15.useRef)(drag);
2859
- const wheelRef = (0, import_react15.useRef)(wheel);
2860
- const dollyRef = (0, import_react15.useRef)(dolly);
2861
- const invertRef = (0, import_react15.useRef)(invert);
2862
- const zoomMinRef = (0, import_react15.useRef)(minZoom);
2863
- const zoomMaxRef = (0, import_react15.useRef)(maxZoom);
2864
- const distanceMinRef = (0, import_react15.useRef)(minDistance);
2865
- const distanceMaxRef = (0, import_react15.useRef)(maxDistance);
2866
- const animateRef = (0, import_react15.useRef)(animate);
2867
- const onChangeRef = (0, import_react15.useRef)(onChange);
2868
- const onInteractionStartRef = (0, import_react15.useRef)(onInteractionStart);
2869
- const onInteractionEndRef = (0, import_react15.useRef)(onInteractionEnd);
2870
- (0, import_react15.useEffect)(() => {
4392
+ const dragRef = (0, import_react17.useRef)(drag);
4393
+ const wheelRef = (0, import_react17.useRef)(wheel);
4394
+ const dollyRef = (0, import_react17.useRef)(dolly);
4395
+ const invertRef = (0, import_react17.useRef)(invert);
4396
+ const zoomMinRef = (0, import_react17.useRef)(minZoom);
4397
+ const zoomMaxRef = (0, import_react17.useRef)(maxZoom);
4398
+ const distanceMinRef = (0, import_react17.useRef)(minDistance);
4399
+ const distanceMaxRef = (0, import_react17.useRef)(maxDistance);
4400
+ const animateRef = (0, import_react17.useRef)(animate);
4401
+ const onChangeRef = (0, import_react17.useRef)(onChange);
4402
+ const onInteractionStartRef = (0, import_react17.useRef)(onInteractionStart);
4403
+ const onInteractionEndRef = (0, import_react17.useRef)(onInteractionEnd);
4404
+ (0, import_react17.useEffect)(() => {
2871
4405
  dragRef.current = drag;
2872
4406
  wheelRef.current = wheel;
2873
4407
  dollyRef.current = dolly;
@@ -2891,7 +4425,7 @@ function PolyMapControls({
2891
4425
  try {
2892
4426
  fn(cameraSnapshot());
2893
4427
  } catch (err) {
2894
- console.error("[polycss/react] PolyMapControls onChange threw:", err);
4428
+ console.error("[polycss/react] PolyOrbitControls onChange threw:", err);
2895
4429
  }
2896
4430
  };
2897
4431
  const fireStart = () => {
@@ -2900,7 +4434,7 @@ function PolyMapControls({
2900
4434
  try {
2901
4435
  fn(cameraSnapshot());
2902
4436
  } catch (err) {
2903
- console.error("[polycss/react] PolyMapControls onInteractionStart threw:", err);
4437
+ console.error("[polycss/react] PolyOrbitControls onInteractionStart threw:", err);
2904
4438
  }
2905
4439
  };
2906
4440
  const fireEnd = () => {
@@ -2909,11 +4443,11 @@ function PolyMapControls({
2909
4443
  try {
2910
4444
  fn(cameraSnapshot());
2911
4445
  } catch (err) {
2912
- console.error("[polycss/react] PolyMapControls onInteractionEnd threw:", err);
4446
+ console.error("[polycss/react] PolyOrbitControls onInteractionEnd threw:", err);
2913
4447
  }
2914
4448
  };
2915
- const animationPausedShared = (0, import_react15.useRef)({ value: false }).current;
2916
- (0, import_react15.useEffect)(() => {
4449
+ const animationPausedShared = (0, import_react17.useRef)({ value: false }).current;
4450
+ (0, import_react17.useEffect)(() => {
2917
4451
  if (!drag) return;
2918
4452
  const el = cameraElRef.current;
2919
4453
  if (!el) return;
@@ -2950,9 +4484,9 @@ function PolyMapControls({
2950
4484
  pointer = { x: e.clientX, y: e.clientY };
2951
4485
  const handle = cameraRef.current;
2952
4486
  if (e.shiftKey) {
2953
- buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2954
- } else {
2955
4487
  buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
4488
+ } else {
4489
+ buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
2956
4490
  }
2957
4491
  applyTransformDirect();
2958
4492
  store.updateCameraFromRef(handle);
@@ -2986,7 +4520,7 @@ function PolyMapControls({
2986
4520
  const dy = e.clientY - rightPointer.y;
2987
4521
  rightPointer = { x: e.clientX, y: e.clientY };
2988
4522
  const handle = cameraRef.current;
2989
- buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
4523
+ buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
2990
4524
  applyTransformDirect();
2991
4525
  store.updateCameraFromRef(handle);
2992
4526
  fireChange();
@@ -3023,12 +4557,12 @@ function PolyMapControls({
3023
4557
  el.style.userSelect = "";
3024
4558
  };
3025
4559
  }, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
3026
- (0, import_react15.useEffect)(
4560
+ (0, import_react17.useEffect)(
3027
4561
  () => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
3028
4562
  // eslint-disable-next-line react-hooks/exhaustive-deps
3029
4563
  [wheel, applyTransformDirect, cameraElRef, cameraRef, store]
3030
4564
  );
3031
- (0, import_react15.useEffect)(
4565
+ (0, import_react17.useEffect)(
3032
4566
  () => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
3033
4567
  // eslint-disable-next-line react-hooks/exhaustive-deps
3034
4568
  [!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
@@ -3037,9 +4571,10 @@ function PolyMapControls({
3037
4571
  }
3038
4572
 
3039
4573
  // src/controls/TransformControls.tsx
3040
- var import_react16 = require("react");
3041
- var import_polycss_core8 = require("@layoutit/polycss-core");
3042
- var import_jsx_runtime7 = require("react/jsx-runtime");
4574
+ var import_react18 = require("react");
4575
+ var import_polycss_core9 = require("@layoutit/polycss-core");
4576
+ var import_jsx_runtime8 = require("react/jsx-runtime");
4577
+ var FALLBACK_CAMERA_STORE = createSceneStore(import_polycss_core9.DEFAULT_CAMERA_STATE);
3043
4578
  var COLOR_X = "#ff3653";
3044
4579
  var COLOR_Y = "#8adb00";
3045
4580
  var COLOR_Z = "#2c8fff";
@@ -3061,8 +4596,10 @@ var SHAFT_HALF_THICKNESS_RATIO = 0.0125;
3061
4596
  var HEAD_LENGTH_RATIO = 0.15;
3062
4597
  var HEAD_HALF_THICKNESS_RATIO = 0.04;
3063
4598
  var RING_RADIUS_RATIO = 1;
3064
- var RING_HALF_THICKNESS_RATIO = 0.012;
3065
- var RING_SEGMENTS = 64;
4599
+ var RING_HALF_THICKNESS_RATIO = 0.02;
4600
+ var RING_QUAD_OUTER_RATIO = 1.04;
4601
+ var PLANE_HALF_SIZE_RATIO = 0.1;
4602
+ var PLANE_OFFSET_RATIO = 0.25;
3066
4603
  var SCREEN_AXIS_DEAD_ZONE_SQ = 1e-4;
3067
4604
  function gizmoLengthForMesh(polygons) {
3068
4605
  if (polygons.length === 0) return FALLBACK_SHAFT_LENGTH;
@@ -3130,6 +4667,23 @@ function userAxisLetterOf(key) {
3130
4667
  const last = key.replace("-", "")[0];
3131
4668
  return last;
3132
4669
  }
4670
+ function isAxisBackFacing(cssAxis, sign, rotXDeg, rotYDeg) {
4671
+ const rx = rotXDeg * Math.PI / 180;
4672
+ const ry = rotYDeg * Math.PI / 180;
4673
+ const a = [0, 0, 0];
4674
+ a[cssAxis] = sign;
4675
+ const bx = a[0] * Math.cos(ry) - a[1] * Math.sin(ry);
4676
+ const by = a[0] * Math.sin(ry) + a[1] * Math.cos(ry);
4677
+ const bz = a[2];
4678
+ const cz = by * Math.sin(rx) + bz * Math.cos(rx);
4679
+ void bx;
4680
+ return cz < 0;
4681
+ }
4682
+ var PLANE_SPECS = [
4683
+ { perpAxis: 2, axisA: 0, axisB: 1, key: "xy", color: COLOR_Z },
4684
+ { perpAxis: 1, axisA: 0, axisB: 2, key: "xz", color: COLOR_Y },
4685
+ { perpAxis: 0, axisA: 1, axisB: 2, key: "yz", color: COLOR_X }
4686
+ ];
3133
4687
  function startAxisDrag(opts) {
3134
4688
  const {
3135
4689
  cssAxis,
@@ -3197,6 +4751,84 @@ function startAxisDrag(opts) {
3197
4751
  window.addEventListener("pointerup", handleUp);
3198
4752
  window.addEventListener("pointercancel", handleUp);
3199
4753
  }
4754
+ function startPlaneDrag(opts) {
4755
+ const {
4756
+ axisA,
4757
+ axisB,
4758
+ probeDistanceCss,
4759
+ wrapper,
4760
+ target,
4761
+ startClientX,
4762
+ startClientY,
4763
+ translationSnap,
4764
+ onChange,
4765
+ onObjectChange,
4766
+ onMouseDown,
4767
+ onMouseUp,
4768
+ onDraggingChanged
4769
+ } = opts;
4770
+ const axisAVec = [0, 0, 0];
4771
+ axisAVec[axisA] = 1;
4772
+ const axisBVec = [0, 0, 0];
4773
+ axisBVec[axisB] = 1;
4774
+ function probe(axisVec) {
4775
+ const el = wrapper.ownerDocument.createElement("div");
4776
+ el.style.position = "absolute";
4777
+ el.style.left = "0";
4778
+ el.style.top = "0";
4779
+ el.style.width = "0";
4780
+ el.style.height = "0";
4781
+ el.style.transform = `translate3d(${axisVec[0] * probeDistanceCss}px, ${axisVec[1] * probeDistanceCss}px, ${axisVec[2] * probeDistanceCss}px)`;
4782
+ wrapper.appendChild(el);
4783
+ const wR = wrapper.getBoundingClientRect();
4784
+ const pR = el.getBoundingClientRect();
4785
+ wrapper.removeChild(el);
4786
+ return {
4787
+ x: (pR.left - wR.left) / probeDistanceCss,
4788
+ y: (pR.top - wR.top) / probeDistanceCss
4789
+ };
4790
+ }
4791
+ const pA = probe(axisAVec);
4792
+ const pB = probe(axisBVec);
4793
+ const det = pA.x * pB.y - pB.x * pA.y;
4794
+ if (Math.abs(det) < SCREEN_AXIS_DEAD_ZONE_SQ) return;
4795
+ const startPos = target.getPosition() ?? [0, 0, 0];
4796
+ onMouseDown?.();
4797
+ onDraggingChanged?.(true);
4798
+ const handleMove = (ev) => {
4799
+ const dx = ev.clientX - startClientX;
4800
+ const dy = ev.clientY - startClientY;
4801
+ let tA = (pB.y * dx - pB.x * dy) / det;
4802
+ let tB = (-pA.y * dx + pA.x * dy) / det;
4803
+ if (translationSnap !== null) {
4804
+ tA = Math.round(tA / translationSnap) * translationSnap;
4805
+ tB = Math.round(tB / translationSnap) * translationSnap;
4806
+ }
4807
+ const next = [
4808
+ startPos[0] + tA * axisAVec[0] + tB * axisBVec[0],
4809
+ startPos[1] + tA * axisAVec[1] + tB * axisBVec[1],
4810
+ startPos[2] + tA * axisAVec[2] + tB * axisBVec[2]
4811
+ ];
4812
+ onObjectChange?.({ object: target, position: next });
4813
+ onChange?.();
4814
+ };
4815
+ const handleUp = () => {
4816
+ window.removeEventListener("pointermove", handleMove);
4817
+ window.removeEventListener("pointerup", handleUp);
4818
+ window.removeEventListener("pointercancel", handleUp);
4819
+ const swallow = (e) => {
4820
+ e.stopPropagation();
4821
+ e.stopImmediatePropagation();
4822
+ };
4823
+ window.addEventListener("click", swallow, { capture: true, once: true });
4824
+ setTimeout(() => window.removeEventListener("click", swallow, true), 0);
4825
+ onMouseUp?.();
4826
+ onDraggingChanged?.(false);
4827
+ };
4828
+ window.addEventListener("pointermove", handleMove);
4829
+ window.addEventListener("pointerup", handleUp);
4830
+ window.addEventListener("pointercancel", handleUp);
4831
+ }
3200
4832
  function startRingDrag(opts) {
3201
4833
  const {
3202
4834
  cssAxis,
@@ -3219,6 +4851,7 @@ function startRingDrag(opts) {
3219
4851
  const startRotation = target.getRotation() ?? [0, 0, 0];
3220
4852
  onMouseDown?.();
3221
4853
  onDraggingChanged?.(true);
4854
+ const qStart = (0, import_polycss_core9.quatFromEulerXYZ)(startRotation);
3222
4855
  const handleMove = (ev) => {
3223
4856
  const a = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
3224
4857
  let d = a - lastAngle;
@@ -3228,12 +4861,10 @@ function startRingDrag(opts) {
3228
4861
  lastAngle = a;
3229
4862
  let degrees = cumulative * 180 / Math.PI;
3230
4863
  degrees = snap(degrees, rotationSnap);
3231
- const newRotation = [
3232
- startRotation[0],
3233
- startRotation[1],
3234
- startRotation[2]
3235
- ];
3236
- newRotation[cssAxis] = startRotation[cssAxis] + degrees;
4864
+ const axisVec = [0, 0, 0];
4865
+ axisVec[cssAxis] = 1;
4866
+ const qDelta = (0, import_polycss_core9.quatFromAxisAngle)(axisVec, degrees * Math.PI / 180);
4867
+ const newRotation = (0, import_polycss_core9.eulerXYZFromQuat)((0, import_polycss_core9.quatMultiply)(qDelta, qStart));
3237
4868
  onObjectChange?.({ object: target, rotation: newRotation });
3238
4869
  onChange?.();
3239
4870
  };
@@ -3272,15 +4903,24 @@ function PolyTransformControls({
3272
4903
  onMouseUp,
3273
4904
  onDraggingChanged
3274
4905
  }) {
3275
- const [, forceRender] = (0, import_react16.useState)(0);
3276
- (0, import_react16.useEffect)(() => {
4906
+ const [, forceRender] = (0, import_react18.useState)(0);
4907
+ (0, import_react18.useEffect)(() => {
3277
4908
  forceRender((n) => n + 1);
3278
4909
  }, [object]);
3279
- const [hoveredKey, setHoveredKey] = (0, import_react16.useState)(null);
3280
- const [draggingKey, setDraggingKey] = (0, import_react16.useState)(null);
3281
- const cameraCtx = (0, import_react16.useContext)(PolyCameraContext);
4910
+ const cameraCtxForRot = (0, import_react18.useContext)(PolyCameraContext);
4911
+ const rotX = useStoreSelector(
4912
+ cameraCtxForRot?.store ?? FALLBACK_CAMERA_STORE,
4913
+ (s) => s.cameraState.rotX
4914
+ );
4915
+ const rotY = useStoreSelector(
4916
+ cameraCtxForRot?.store ?? FALLBACK_CAMERA_STORE,
4917
+ (s) => s.cameraState.rotY
4918
+ );
4919
+ const [hoveredKey, setHoveredKey] = (0, import_react18.useState)(null);
4920
+ const [draggingKey, setDraggingKey] = (0, import_react18.useState)(null);
4921
+ const cameraCtx = (0, import_react18.useContext)(PolyCameraContext);
3282
4922
  const cameraElRef = cameraCtx?.cameraElRef;
3283
- const dragRef = (0, import_react16.useRef)({
4923
+ const dragRef = (0, import_react18.useRef)({
3284
4924
  target: null,
3285
4925
  mode: "translate",
3286
4926
  shaftLengthCss: 0,
@@ -3289,7 +4929,7 @@ function PolyTransformControls({
3289
4929
  translationSnap: null,
3290
4930
  rotationSnap: null
3291
4931
  });
3292
- (0, import_react16.useEffect)(() => {
4932
+ (0, import_react18.useEffect)(() => {
3293
4933
  const cameraEl = cameraElRef?.current;
3294
4934
  if (!cameraEl) return;
3295
4935
  const onPointerDown = (event) => {
@@ -3298,6 +4938,42 @@ function PolyTransformControls({
3298
4938
  const targetEl = event.target;
3299
4939
  if (targetEl?.closest(".polycss-transform-gizmo")) return;
3300
4940
  if (state.mode === "translate") {
4941
+ for (const spec of PLANE_SPECS) {
4942
+ const aL = ["x", "y", "z"][spec.axisA];
4943
+ const bL = ["x", "y", "z"][spec.axisB];
4944
+ if (!state.show[aL] || !state.show[bL]) continue;
4945
+ const planeEl = document.querySelector(
4946
+ `.polycss-transform-plane--${spec.key}`
4947
+ );
4948
+ if (!planeEl) continue;
4949
+ if (!pointInMeshElement(planeEl, event.clientX, event.clientY)) continue;
4950
+ event.preventDefault();
4951
+ event.stopPropagation();
4952
+ const wrapper = planeEl.closest(
4953
+ "[data-poly-transform-controls]"
4954
+ );
4955
+ if (!wrapper) return;
4956
+ setDraggingKey(spec.key);
4957
+ startPlaneDrag({
4958
+ axisA: spec.axisA,
4959
+ axisB: spec.axisB,
4960
+ probeDistanceCss: state.shaftLengthCss,
4961
+ wrapper,
4962
+ target: state.target,
4963
+ startClientX: event.clientX,
4964
+ startClientY: event.clientY,
4965
+ translationSnap: state.translationSnap,
4966
+ onChange: state.onChange,
4967
+ onObjectChange: state.onObjectChange,
4968
+ onMouseDown: state.onMouseDown,
4969
+ onMouseUp: state.onMouseUp,
4970
+ onDraggingChanged: (d) => {
4971
+ if (!d) setDraggingKey(null);
4972
+ state.onDraggingChanged?.(d);
4973
+ }
4974
+ });
4975
+ return;
4976
+ }
3301
4977
  for (const spec of ARROW_SPECS) {
3302
4978
  if (!state.show[userAxisLetterOf(spec.key)]) continue;
3303
4979
  const arrowEl = document.querySelector(
@@ -3397,9 +5073,9 @@ function PolyTransformControls({
3397
5073
  onDraggingChanged
3398
5074
  };
3399
5075
  const wrapperStyle = {
5076
+ transform: `translate3d(${wrapperPos[0]}px, ${wrapperPos[1]}px, ${wrapperPos[2]}px)`,
3400
5077
  position: "absolute",
3401
5078
  transformStyle: "preserve-3d",
3402
- transform: `translate3d(${wrapperPos[0]}px, ${wrapperPos[1]}px, ${wrapperPos[2]}px)`,
3403
5079
  // No `pointer-events: none` here — that property is inherited, so
3404
5080
  // setting it on the wrapper would cascade to every arrow polygon
3405
5081
  // and disable native hit-testing on the gizmo entirely. The
@@ -3407,7 +5083,7 @@ function PolyTransformControls({
3407
5083
  // own; descendants opt in via the default `auto`.
3408
5084
  zIndex: 1e3
3409
5085
  };
3410
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
5086
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3411
5087
  "div",
3412
5088
  {
3413
5089
  className: "polycss-transform-controls",
@@ -3422,7 +5098,8 @@ function PolyTransformControls({
3422
5098
  const hovered = hoveredKey === spec.key;
3423
5099
  const dragging = draggingKey === spec.key;
3424
5100
  const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
3425
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5101
+ const backFacing = isAxisBackFacing(spec.cssAxis, spec.sign, rotX, rotY);
5102
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3426
5103
  TranslateArrow,
3427
5104
  {
3428
5105
  cssAxis: spec.cssAxis,
@@ -3430,6 +5107,41 @@ function PolyTransformControls({
3430
5107
  axisKey: spec.key,
3431
5108
  color: withAlpha(spec.color, alpha),
3432
5109
  shaftLengthCss,
5110
+ includeShaft: !backFacing,
5111
+ target,
5112
+ enabled,
5113
+ translationSnap,
5114
+ onChange,
5115
+ onObjectChange,
5116
+ onMouseDown,
5117
+ onMouseUp,
5118
+ onDraggingChanged,
5119
+ onHoverChange: (h) => setHoveredKey(h ? spec.key : (cur) => cur === spec.key ? null : cur),
5120
+ onDraggingStart: () => setDraggingKey(spec.key),
5121
+ onDraggingStop: () => setDraggingKey((cur) => cur === spec.key ? null : cur)
5122
+ },
5123
+ spec.key
5124
+ );
5125
+ }),
5126
+ mode === "translate" && PLANE_SPECS.map((spec) => {
5127
+ const aLetter = ["x", "y", "z"][spec.axisA];
5128
+ const bLetter = ["x", "y", "z"][spec.axisB];
5129
+ const show = { x: showX, y: showY, z: showZ }[aLetter] && { x: showX, y: showY, z: showZ }[bLetter];
5130
+ if (!show) return null;
5131
+ const hovered = hoveredKey === spec.key;
5132
+ const dragging = draggingKey === spec.key;
5133
+ const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
5134
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
5135
+ TranslatePlane,
5136
+ {
5137
+ axisA: spec.axisA,
5138
+ axisB: spec.axisB,
5139
+ perpAxis: spec.perpAxis,
5140
+ planeKey: spec.key,
5141
+ color: withAlpha(spec.color, alpha),
5142
+ shaftLengthCss,
5143
+ rotX,
5144
+ rotY,
3433
5145
  target,
3434
5146
  enabled,
3435
5147
  translationSnap,
@@ -3451,7 +5163,7 @@ function PolyTransformControls({
3451
5163
  const hovered = hoveredKey === spec.key;
3452
5164
  const dragging = draggingKey === spec.key;
3453
5165
  const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
3454
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5166
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3455
5167
  RotateRing,
3456
5168
  {
3457
5169
  cssAxis: spec.cssAxis,
@@ -3483,6 +5195,7 @@ function TranslateArrow({
3483
5195
  axisKey,
3484
5196
  color,
3485
5197
  shaftLengthCss,
5198
+ includeShaft,
3486
5199
  target,
3487
5200
  enabled,
3488
5201
  translationSnap,
@@ -3495,7 +5208,7 @@ function TranslateArrow({
3495
5208
  onDraggingStart,
3496
5209
  onDraggingStop
3497
5210
  }) {
3498
- const cbRef = (0, import_react16.useRef)({
5211
+ const cbRef = (0, import_react18.useRef)({
3499
5212
  onChange,
3500
5213
  onObjectChange,
3501
5214
  onMouseDown,
@@ -3517,19 +5230,20 @@ function TranslateArrow({
3517
5230
  enabled,
3518
5231
  translationSnap
3519
5232
  };
3520
- const polygons = (0, import_react16.useMemo)(() => {
5233
+ const polygons = (0, import_react18.useMemo)(() => {
3521
5234
  const lengthWorld = shaftLengthCss / SCENE_TILE_SIZE;
3522
- return (0, import_polycss_core8.arrowPolygons)({
5235
+ return (0, import_polycss_core9.arrowPolygons)({
3523
5236
  axis: WORLD_AXIS_FOR_CSS[cssAxis],
3524
5237
  sign,
3525
5238
  shaftLength: lengthWorld,
3526
5239
  shaftHalfThickness: lengthWorld * SHAFT_HALF_THICKNESS_RATIO,
3527
5240
  headLength: lengthWorld * HEAD_LENGTH_RATIO,
3528
5241
  headHalfThickness: lengthWorld * HEAD_HALF_THICKNESS_RATIO,
3529
- color
5242
+ color,
5243
+ shaft: includeShaft
3530
5244
  });
3531
- }, [cssAxis, sign, color, shaftLengthCss]);
3532
- const onPointerDown = (0, import_react16.useCallback)(
5245
+ }, [cssAxis, sign, color, shaftLengthCss, includeShaft]);
5246
+ const onPointerDown = (0, import_react18.useCallback)(
3533
5247
  (e) => {
3534
5248
  if (!cbRef.current.enabled) return;
3535
5249
  e.stopPropagation();
@@ -3558,7 +5272,7 @@ function TranslateArrow({
3558
5272
  },
3559
5273
  [cssAxis, sign, target, shaftLengthCss]
3560
5274
  );
3561
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5275
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3562
5276
  PolyMesh,
3563
5277
  {
3564
5278
  polygons,
@@ -3570,6 +5284,107 @@ function TranslateArrow({
3570
5284
  }
3571
5285
  );
3572
5286
  }
5287
+ function TranslatePlane({
5288
+ axisA,
5289
+ axisB,
5290
+ perpAxis,
5291
+ planeKey,
5292
+ color,
5293
+ shaftLengthCss,
5294
+ rotX,
5295
+ rotY,
5296
+ target,
5297
+ enabled,
5298
+ translationSnap,
5299
+ onChange,
5300
+ onObjectChange,
5301
+ onMouseDown,
5302
+ onMouseUp,
5303
+ onDraggingChanged,
5304
+ onHoverChange,
5305
+ onDraggingStart,
5306
+ onDraggingStop
5307
+ }) {
5308
+ const cbRef = (0, import_react18.useRef)({
5309
+ onChange,
5310
+ onObjectChange,
5311
+ onMouseDown,
5312
+ onMouseUp,
5313
+ onDraggingChanged,
5314
+ onDraggingStart,
5315
+ onDraggingStop,
5316
+ enabled,
5317
+ translationSnap
5318
+ });
5319
+ cbRef.current = {
5320
+ onChange,
5321
+ onObjectChange,
5322
+ onMouseDown,
5323
+ onMouseUp,
5324
+ onDraggingChanged,
5325
+ onDraggingStart,
5326
+ onDraggingStop,
5327
+ enabled,
5328
+ translationSnap
5329
+ };
5330
+ const polygons = (0, import_react18.useMemo)(() => {
5331
+ const lengthWorld = shaftLengthCss / SCENE_TILE_SIZE;
5332
+ const worldPerp = WORLD_AXIS_FOR_CSS[perpAxis];
5333
+ const worldA = (worldPerp + 1) % 3;
5334
+ const worldB = (worldPerp + 2) % 3;
5335
+ const cssAForOffset = WORLD_AXIS_FOR_CSS[worldA];
5336
+ const cssBForOffset = WORLD_AXIS_FOR_CSS[worldB];
5337
+ const signA = isAxisBackFacing(cssAForOffset, 1, rotX, rotY) ? -1 : 1;
5338
+ const signB = isAxisBackFacing(cssBForOffset, 1, rotX, rotY) ? -1 : 1;
5339
+ const mag = lengthWorld * PLANE_OFFSET_RATIO;
5340
+ return (0, import_polycss_core9.planePolygons)({
5341
+ axis: worldPerp,
5342
+ size: lengthWorld * PLANE_HALF_SIZE_RATIO,
5343
+ offset: [signA * mag, signB * mag],
5344
+ color
5345
+ });
5346
+ }, [perpAxis, color, shaftLengthCss, rotX, rotY]);
5347
+ const onPointerDown = (0, import_react18.useCallback)(
5348
+ (e) => {
5349
+ if (!cbRef.current.enabled) return;
5350
+ e.stopPropagation();
5351
+ const meshEl = e.eventObject.element;
5352
+ const wrapper = meshEl?.closest("[data-poly-transform-controls]");
5353
+ if (!wrapper) return;
5354
+ cbRef.current.onDraggingStart?.();
5355
+ startPlaneDrag({
5356
+ axisA,
5357
+ axisB,
5358
+ probeDistanceCss: shaftLengthCss,
5359
+ wrapper,
5360
+ target,
5361
+ startClientX: e.nativeEvent.clientX,
5362
+ startClientY: e.nativeEvent.clientY,
5363
+ translationSnap: cbRef.current.translationSnap,
5364
+ onChange: cbRef.current.onChange,
5365
+ onObjectChange: cbRef.current.onObjectChange,
5366
+ onMouseDown: cbRef.current.onMouseDown,
5367
+ onMouseUp: cbRef.current.onMouseUp,
5368
+ onDraggingChanged: (d) => {
5369
+ if (!d) cbRef.current.onDraggingStop?.();
5370
+ cbRef.current.onDraggingChanged?.(d);
5371
+ }
5372
+ });
5373
+ },
5374
+ [axisA, axisB, target, shaftLengthCss]
5375
+ );
5376
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
5377
+ PolyMesh,
5378
+ {
5379
+ polygons,
5380
+ onPointerDown,
5381
+ onPointerOver: () => onHoverChange?.(true),
5382
+ onPointerOut: () => onHoverChange?.(false),
5383
+ className: `polycss-transform-gizmo polycss-transform-plane polycss-transform-plane--${planeKey}`,
5384
+ textureLighting: "baked"
5385
+ }
5386
+ );
5387
+ }
3573
5388
  function RotateRing({
3574
5389
  cssAxis,
3575
5390
  axisKey,
@@ -3587,7 +5402,7 @@ function RotateRing({
3587
5402
  onDraggingStart,
3588
5403
  onDraggingStop
3589
5404
  }) {
3590
- const cbRef = (0, import_react16.useRef)({
5405
+ const cbRef = (0, import_react18.useRef)({
3591
5406
  onChange,
3592
5407
  onObjectChange,
3593
5408
  onMouseDown,
@@ -3609,17 +5424,18 @@ function RotateRing({
3609
5424
  enabled,
3610
5425
  rotationSnap
3611
5426
  };
3612
- const polygons = (0, import_react16.useMemo)(() => {
5427
+ const polygons = (0, import_react18.useMemo)(() => {
3613
5428
  const radiusWorld = radiusCss / SCENE_TILE_SIZE;
3614
- return (0, import_polycss_core8.ringPolygons)({
5429
+ const outerWorld = radiusWorld * RING_QUAD_OUTER_RATIO;
5430
+ return (0, import_polycss_core9.ringQuadPolygons)({
3615
5431
  axis: WORLD_AXIS_FOR_CSS[cssAxis],
3616
- radius: radiusWorld,
3617
- halfThickness: radiusWorld * RING_HALF_THICKNESS_RATIO,
3618
- segments: RING_SEGMENTS,
5432
+ outerRadius: outerWorld,
3619
5433
  color
3620
5434
  });
3621
5435
  }, [cssAxis, color, radiusCss]);
3622
- const onPointerDown = (0, import_react16.useCallback)(
5436
+ const ringInnerRatio = (1 - RING_HALF_THICKNESS_RATIO) / RING_QUAD_OUTER_RATIO;
5437
+ const ringOuterRatio = (1 + RING_HALF_THICKNESS_RATIO) / RING_QUAD_OUTER_RATIO;
5438
+ const onPointerDown = (0, import_react18.useCallback)(
3623
5439
  (e) => {
3624
5440
  if (!cbRef.current.enabled) return;
3625
5441
  e.stopPropagation();
@@ -3646,7 +5462,7 @@ function RotateRing({
3646
5462
  },
3647
5463
  [cssAxis, target]
3648
5464
  );
3649
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5465
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3650
5466
  PolyMesh,
3651
5467
  {
3652
5468
  polygons,
@@ -3654,15 +5470,19 @@ function RotateRing({
3654
5470
  onPointerOver: () => onHoverChange?.(true),
3655
5471
  onPointerOut: () => onHoverChange?.(false),
3656
5472
  className: `polycss-transform-gizmo polycss-transform-ring polycss-transform-ring--${axisKey}`,
5473
+ style: {
5474
+ ["--ring-inner-ratio"]: ringInnerRatio,
5475
+ ["--ring-outer-ratio"]: ringOuterRatio
5476
+ },
3657
5477
  textureLighting: "baked"
3658
5478
  }
3659
5479
  );
3660
5480
  }
3661
5481
 
3662
5482
  // src/select/Select.tsx
3663
- var import_react17 = require("react");
3664
- var import_jsx_runtime8 = require("react/jsx-runtime");
3665
- var SelectContext = (0, import_react17.createContext)(null);
5483
+ var import_react19 = require("react");
5484
+ var import_jsx_runtime9 = require("react/jsx-runtime");
5485
+ var SelectContext = (0, import_react19.createContext)(null);
3666
5486
  function PolySelect({
3667
5487
  multiple = false,
3668
5488
  filter,
@@ -3673,19 +5493,19 @@ function PolySelect({
3673
5493
  className,
3674
5494
  style
3675
5495
  }) {
3676
- const [selected, setSelectedState] = (0, import_react17.useState)([]);
3677
- const filterRef = (0, import_react17.useRef)(filter);
5496
+ const [selected, setSelectedState] = (0, import_react19.useState)([]);
5497
+ const filterRef = (0, import_react19.useRef)(filter);
3678
5498
  filterRef.current = filter;
3679
- const onChangeRef = (0, import_react17.useRef)(onChange);
5499
+ const onChangeRef = (0, import_react19.useRef)(onChange);
3680
5500
  onChangeRef.current = onChange;
3681
- const multipleRef = (0, import_react17.useRef)(multiple);
5501
+ const multipleRef = (0, import_react19.useRef)(multiple);
3682
5502
  multipleRef.current = multiple;
3683
- const apply = (0, import_react17.useCallback)((next) => {
5503
+ const apply = (0, import_react19.useCallback)((next) => {
3684
5504
  const filtered = filterRef.current ? filterRef.current(next) : next;
3685
5505
  setSelectedState(filtered);
3686
5506
  if (onChangeRef.current) onChangeRef.current(filtered);
3687
5507
  }, []);
3688
- const api = (0, import_react17.useMemo)(() => {
5508
+ const api = (0, import_react19.useMemo)(() => {
3689
5509
  return {
3690
5510
  selected,
3691
5511
  set: (next) => apply(next),
@@ -3718,14 +5538,14 @@ function PolySelect({
3718
5538
  has: (h) => selected.includes(h)
3719
5539
  };
3720
5540
  }, [selected, apply]);
3721
- const wrapperRef = (0, import_react17.useRef)(null);
3722
- const cameraCtx = (0, import_react17.useContext)(PolyCameraContext);
5541
+ const wrapperRef = (0, import_react19.useRef)(null);
5542
+ const cameraCtx = (0, import_react19.useContext)(PolyCameraContext);
3723
5543
  const cameraElRef = cameraCtx?.cameraElRef;
3724
- const clearOnMissRef = (0, import_react17.useRef)(clearOnMiss);
5544
+ const clearOnMissRef = (0, import_react19.useRef)(clearOnMiss);
3725
5545
  clearOnMissRef.current = clearOnMiss;
3726
- const onPointerMissedRef = (0, import_react17.useRef)(onPointerMissed);
5546
+ const onPointerMissedRef = (0, import_react19.useRef)(onPointerMissed);
3727
5547
  onPointerMissedRef.current = onPointerMissed;
3728
- const findMeshUnderPoint2 = (0, import_react17.useCallback)(
5548
+ const findMeshUnderPoint2 = (0, import_react19.useCallback)(
3729
5549
  (clientX, clientY) => findMeshUnderPoint(
3730
5550
  clientX,
3731
5551
  clientY,
@@ -3733,7 +5553,7 @@ function PolySelect({
3733
5553
  ),
3734
5554
  []
3735
5555
  );
3736
- (0, import_react17.useEffect)(() => {
5556
+ (0, import_react19.useEffect)(() => {
3737
5557
  const cameraEl = cameraElRef?.current;
3738
5558
  if (!cameraEl) return;
3739
5559
  const onClick = (event) => {
@@ -3780,7 +5600,7 @@ function PolySelect({
3780
5600
  }
3781
5601
  };
3782
5602
  const wrapperStyle = { display: "contents", ...style };
3783
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectContext.Provider, { value: api, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
5603
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(SelectContext.Provider, { value: api, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3784
5604
  "div",
3785
5605
  {
3786
5606
  ref: wrapperRef,
@@ -3793,10 +5613,10 @@ function PolySelect({
3793
5613
  ) });
3794
5614
  }
3795
5615
  function usePolySelect() {
3796
- return (0, import_react17.useContext)(SelectContext)?.selected ?? [];
5616
+ return (0, import_react19.useContext)(SelectContext)?.selected ?? [];
3797
5617
  }
3798
5618
  function usePolySelectionApi() {
3799
- const ctx = (0, import_react17.useContext)(SelectContext);
5619
+ const ctx = (0, import_react19.useContext)(SelectContext);
3800
5620
  if (!ctx) {
3801
5621
  throw new Error("polycss: usePolySelectionApi must be used inside <PolySelect>.");
3802
5622
  }
@@ -3804,9 +5624,9 @@ function usePolySelectionApi() {
3804
5624
  }
3805
5625
 
3806
5626
  // src/helpers/PolyAxesHelper.tsx
3807
- var import_react18 = require("react");
3808
- var import_polycss_core9 = require("@layoutit/polycss-core");
3809
- var import_jsx_runtime9 = require("react/jsx-runtime");
5627
+ var import_react20 = require("react");
5628
+ var import_polycss_core10 = require("@layoutit/polycss-core");
5629
+ var import_jsx_runtime10 = require("react/jsx-runtime");
3810
5630
  function PolyAxesHelper({
3811
5631
  size,
3812
5632
  thickness,
@@ -3815,17 +5635,17 @@ function PolyAxesHelper({
3815
5635
  yColor,
3816
5636
  zColor
3817
5637
  }) {
3818
- const polygons = (0, import_react18.useMemo)(
3819
- () => (0, import_polycss_core9.axesHelperPolygons)({ size, thickness, negative, xColor, yColor, zColor }),
5638
+ const polygons = (0, import_react20.useMemo)(
5639
+ () => (0, import_polycss_core10.axesHelperPolygons)({ size, thickness, negative, xColor, yColor, zColor }),
3820
5640
  [size, thickness, negative, xColor, yColor, zColor]
3821
5641
  );
3822
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PolyMesh, { polygons });
5642
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PolyMesh, { polygons });
3823
5643
  }
3824
5644
 
3825
5645
  // src/helpers/PolyDirectionalLightHelper.tsx
3826
- var import_react19 = require("react");
3827
- var import_polycss_core10 = require("@layoutit/polycss-core");
3828
- var import_jsx_runtime10 = require("react/jsx-runtime");
5646
+ var import_react21 = require("react");
5647
+ var import_polycss_core11 = require("@layoutit/polycss-core");
5648
+ var import_jsx_runtime11 = require("react/jsx-runtime");
3829
5649
  var TILE = 50;
3830
5650
  function PolyDirectionalLightHelper({
3831
5651
  light,
@@ -3835,11 +5655,11 @@ function PolyDirectionalLightHelper({
3835
5655
  color
3836
5656
  }) {
3837
5657
  const swatch = color ?? light.color ?? "#ffd54a";
3838
- const polygons = (0, import_react19.useMemo)(
3839
- () => (0, import_polycss_core10.octahedronPolygons)({ center: [0, 0, 0], size, color: swatch }),
5658
+ const polygons = (0, import_react21.useMemo)(
5659
+ () => (0, import_polycss_core11.octahedronPolygons)({ center: [0, 0, 0], size, color: swatch }),
3840
5660
  [size, swatch]
3841
5661
  );
3842
- const meshPosition = (0, import_react19.useMemo)(() => {
5662
+ const meshPosition = (0, import_react21.useMemo)(() => {
3843
5663
  const [dx, dy, dz] = light.direction;
3844
5664
  const len = Math.hypot(dx, dy, dz) || 1;
3845
5665
  const tx = target?.[0] ?? 0;
@@ -3850,24 +5670,24 @@ function PolyDirectionalLightHelper({
3850
5670
  const worldZ = tz + dz / len * distance;
3851
5671
  return [worldY * TILE, worldX * TILE, worldZ * TILE];
3852
5672
  }, [light.direction, target, distance]);
3853
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PolyMesh, { polygons, position: meshPosition });
5673
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PolyMesh, { polygons, position: meshPosition });
3854
5674
  }
3855
5675
 
3856
5676
  // src/styles/colorResolver.ts
3857
- var import_polycss_core11 = require("@layoutit/polycss-core");
5677
+ var import_polycss_core12 = require("@layoutit/polycss-core");
3858
5678
 
3859
5679
  // src/animation/usePolyAnimation.ts
3860
- var import_react20 = require("react");
3861
- var import_polycss_core12 = require("@layoutit/polycss-core");
5680
+ var import_react22 = require("react");
5681
+ var import_polycss_core13 = require("@layoutit/polycss-core");
3862
5682
  function resolveRoot(rootArg) {
3863
5683
  if (!rootArg) return null;
3864
5684
  if ("current" in rootArg) return rootArg.current;
3865
5685
  return rootArg;
3866
5686
  }
3867
5687
  function usePolyAnimation(clips, controller, root) {
3868
- const internalRef = (0, import_react20.useRef)(null);
3869
- const mixerRef = (0, import_react20.useRef)(null);
3870
- (0, import_react20.useEffect)(() => {
5688
+ const internalRef = (0, import_react22.useRef)(null);
5689
+ const mixerRef = (0, import_react22.useRef)(null);
5690
+ (0, import_react22.useEffect)(() => {
3871
5691
  if (!clips || clips.length === 0 || !controller) {
3872
5692
  mixerRef.current = null;
3873
5693
  return;
@@ -3877,14 +5697,14 @@ function usePolyAnimation(clips, controller, root) {
3877
5697
  mixerRef.current = null;
3878
5698
  return;
3879
5699
  }
3880
- mixerRef.current = (0, import_polycss_core12.createPolyAnimationMixer)(resolvedRoot, controller);
5700
+ mixerRef.current = (0, import_polycss_core13.createPolyAnimationMixer)(resolvedRoot, controller);
3881
5701
  return () => {
3882
5702
  mixerRef.current?.stopAllAction();
3883
5703
  mixerRef.current?.uncacheRoot();
3884
5704
  mixerRef.current = null;
3885
5705
  };
3886
5706
  }, [clips, controller]);
3887
- (0, import_react20.useEffect)(() => {
5707
+ (0, import_react22.useEffect)(() => {
3888
5708
  if (!clips || clips.length === 0 || !controller) return;
3889
5709
  let rafId;
3890
5710
  let lastTime = null;
@@ -3907,7 +5727,7 @@ function usePolyAnimation(clips, controller, root) {
3907
5727
  }, [clips, controller]);
3908
5728
  const resolvedClips = clips ?? [];
3909
5729
  const resolvedNames = resolvedClips.map((c) => c.name);
3910
- const actions = (0, import_react20.useMemo)(() => {
5730
+ const actions = (0, import_react22.useMemo)(() => {
3911
5731
  const target = {};
3912
5732
  for (const clip of resolvedClips) {
3913
5733
  Object.defineProperty(target, clip.name, {
@@ -3935,10 +5755,11 @@ function usePolyAnimation(clips, controller, root) {
3935
5755
  }
3936
5756
 
3937
5757
  // src/index.ts
3938
- var import_polycss_core13 = require("@layoutit/polycss-core");
5758
+ var import_polycss_core14 = require("@layoutit/polycss-core");
3939
5759
  // Annotate the CommonJS export names for ESM import in node:
3940
5760
  0 && (module.exports = {
3941
5761
  BASE_TILE,
5762
+ CAMERA_BACKFACE_CULL_EPS,
3942
5763
  DEFAULT_CAMERA_STATE,
3943
5764
  DEFAULT_PROJECTION,
3944
5765
  LoopOnce,
@@ -3949,6 +5770,8 @@ var import_polycss_core13 = require("@layoutit/polycss-core");
3949
5770
  PolyCamera,
3950
5771
  PolyCameraContext,
3951
5772
  PolyDirectionalLightHelper,
5773
+ PolyFirstPersonControls,
5774
+ PolyGround,
3952
5775
  PolyMapControls,
3953
5776
  PolyMesh,
3954
5777
  PolyOrbitControls,
@@ -3957,11 +5780,19 @@ var import_polycss_core13 = require("@layoutit/polycss-core");
3957
5780
  PolyScene,
3958
5781
  PolySelect,
3959
5782
  PolyTransformControls,
5783
+ VOXEL_CAMERA_CULL_AXIS_EPS,
5784
+ VOXEL_CAMERA_CULL_NORMAL_LIMIT,
3960
5785
  arrowPolygons,
3961
5786
  axesHelperPolygons,
3962
5787
  bakeSolidTextureSampledPolygons,
3963
5788
  bakeSolidTextureSamples,
5789
+ boxPolygons,
3964
5790
  buildSceneContext,
5791
+ cameraCullNormalGroups,
5792
+ cameraCullNormalGroupsFromPolygons,
5793
+ cameraCullNormalKey,
5794
+ cameraCullVisibleSignature,
5795
+ cameraFacingDepth,
3965
5796
  clampChannel,
3966
5797
  computeSceneBbox,
3967
5798
  computeShapeLighting,
@@ -3975,11 +5806,15 @@ var import_polycss_core13 = require("@layoutit/polycss-core");
3975
5806
  formatColor,
3976
5807
  injectPolyBaseStyles,
3977
5808
  inverseRotateVec3,
5809
+ isAxisAlignedSurfaceNormal,
5810
+ isVoxelCameraCullableNormalGroups,
3978
5811
  loadMesh,
3979
5812
  mergePolygons,
5813
+ normalFacesCamera,
3980
5814
  normalizeInvertMultiplier,
3981
5815
  normalizePolygons,
3982
5816
  octahedronPolygons,
5817
+ optimizeMeshPolygons,
3983
5818
  parseColor,
3984
5819
  parseGltf,
3985
5820
  parseHexColor,
@@ -3989,7 +5824,9 @@ var import_polycss_core13 = require("@layoutit/polycss-core");
3989
5824
  parseRgbColor,
3990
5825
  parseVox,
3991
5826
  pointInMeshElement,
5827
+ polygonCssSurfaceNormal,
3992
5828
  polygonFaces,
5829
+ polygonFacesCamera,
3993
5830
  ringPolygons,
3994
5831
  rotateVec3,
3995
5832
  shadeColor,