@layoutit/polycss-react 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ import * as React from 'react';
2
+ import { CSSProperties, MouseEventHandler, PointerEventHandler, FocusEventHandler, KeyboardEventHandler, ReactNode } from 'react';
3
+ import * as _layoutit_polycss_core from '@layoutit/polycss-core';
4
+ import { Vec3, PolyTextureImageSource, PolyTexturePresentation, Vec2, PolyMaterial, PolyDirectionalLight, PolyTextureLightingMode, TextureQuality, PolyTextureLeafSizing, PolyTextureImageRendering, PolyTextureBackend, PolyTextureProjection, LoadMeshOptions, Polygon, ParseResult, PolySeamBleed, MeshResolution } from '@layoutit/polycss-core';
5
+
6
+ /** Three.js-style transform props accepted by every PolyCSS component. */
7
+ interface TransformProps {
8
+ position?: Vec3;
9
+ scale?: number | Vec3;
10
+ rotation?: Vec3;
11
+ }
12
+ /**
13
+ * DOM event handlers, ARIA, and style props forwarded to the rendered
14
+ * element by every Poly component.
15
+ *
16
+ * This is the DOM-native pitch: polygons are real DOM nodes you can
17
+ * target with CSS, attach event handlers to, and inspect in DevTools.
18
+ */
19
+ interface DOMPassthroughProps {
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ id?: string;
23
+ onClick?: MouseEventHandler<HTMLElement>;
24
+ onDoubleClick?: MouseEventHandler<HTMLElement>;
25
+ onMouseEnter?: MouseEventHandler<HTMLElement>;
26
+ onMouseLeave?: MouseEventHandler<HTMLElement>;
27
+ onMouseMove?: MouseEventHandler<HTMLElement>;
28
+ onPointerDown?: PointerEventHandler<HTMLElement>;
29
+ onPointerUp?: PointerEventHandler<HTMLElement>;
30
+ onPointerEnter?: PointerEventHandler<HTMLElement>;
31
+ onPointerLeave?: PointerEventHandler<HTMLElement>;
32
+ onFocus?: FocusEventHandler<HTMLElement>;
33
+ onBlur?: FocusEventHandler<HTMLElement>;
34
+ onKeyDown?: KeyboardEventHandler<HTMLElement>;
35
+ tabIndex?: number;
36
+ role?: string;
37
+ "aria-label"?: string;
38
+ "aria-hidden"?: boolean;
39
+ pointerEvents?: "auto" | "none";
40
+ [dataAttr: `data-${string}`]: string | number | boolean | undefined;
41
+ }
42
+ /**
43
+ * Props for the `<Poly>` component — the atomic polygon primitive.
44
+ *
45
+ * Extends TransformProps + DOMPassthroughProps with the polygon's own fields.
46
+ * This is the canonical Poly component API.
47
+ */
48
+ interface PolyProps extends TransformProps, DOMPassthroughProps {
49
+ vertices: Vec3[];
50
+ color?: string;
51
+ texture?: string;
52
+ textureImageSource?: PolyTextureImageSource;
53
+ texturePresentation?: PolyTexturePresentation;
54
+ uvs?: Vec2[];
55
+ data?: Record<string, string | number | boolean>;
56
+ doubleSided?: boolean;
57
+ /** Shared material. When set AND the polygon's UVs form an axis-aligned
58
+ * rectangle, renders via `background-image` directly — no per-polygon
59
+ * canvas rasterization. Falls back to the atlas path otherwise. */
60
+ material?: PolyMaterial;
61
+ context?: {
62
+ tileSize?: number;
63
+ layerElevation?: number;
64
+ directionalLight?: PolyDirectionalLight;
65
+ textureLighting?: PolyTextureLightingMode;
66
+ textureQuality?: TextureQuality;
67
+ textureLeafSizing?: PolyTextureLeafSizing;
68
+ textureImageRendering?: PolyTextureImageRendering;
69
+ textureBackend?: PolyTextureBackend;
70
+ textureProjection?: PolyTextureProjection;
71
+ debugShowBackfaces?: boolean;
72
+ [key: string]: unknown;
73
+ };
74
+ /** Textured polygon lighting mode. Defaults to scene context, then "baked". */
75
+ textureLighting?: PolyTextureLightingMode;
76
+ /** Atlas bitmap budget and CSS sprite size. `"auto"` (default) uses a
77
+ * device-appropriate memory budget and desktop/mobile sprite sizing. */
78
+ textureQuality?: TextureQuality;
79
+ /** Atlas leaf CSS primitive sizing. Defaults to scene context, then canonical. */
80
+ textureLeafSizing?: PolyTextureLeafSizing;
81
+ /** Default image filtering for atlas and direct image texture leaves. */
82
+ textureImageRendering?: PolyTextureImageRendering;
83
+ /** Default texture backend request. Defaults to scene context, then "auto". */
84
+ textureBackend?: PolyTextureBackend;
85
+ /** Default texture projection request. Defaults to scene context, then "affine". */
86
+ textureProjection?: PolyTextureProjection;
87
+ /** Pre-computed shaded base color from the parent (optional override). */
88
+ baseColor?: string;
89
+ }
90
+
91
+ type UseMeshOptions = LoadMeshOptions;
92
+ interface UseMeshResult {
93
+ polygons: Polygon[];
94
+ voxelSource: ParseResult["voxelSource"];
95
+ loading: boolean;
96
+ error: Error | null;
97
+ warnings: string[];
98
+ /** Manually trigger cleanup (also called on unmount automatically). */
99
+ dispose: () => void;
100
+ }
101
+ declare function usePolyMesh(src: string, options?: UseMeshOptions): UseMeshResult;
102
+
103
+ /**
104
+ * Pointer event API for <PolyMesh>. Mirrors @react-three/fiber's mesh
105
+ * event surface (handler names + payload shape) so devs migrating from
106
+ * three.js use the same mental model. PolyCSS is DOM-native so we get
107
+ * native pointer events for free — no raycaster, no canvas event
108
+ * synthesis.
109
+ *
110
+ * Diverges from r3f in two intentional ways:
111
+ * 1. Bubbling matches DOM (pointer hits the front element only). r3f
112
+ * replays events to occluded objects behind the front hit; doing
113
+ * that here would require running our own raycaster, which defeats
114
+ * the point of being DOM-native.
115
+ * 2. `face`, `uv`, `uv1`, `instanceId` are omitted — they're three.js
116
+ * BufferGeometry / InstancedMesh concepts with no polycss analogue.
117
+ * The polycss-native equivalent of `face` is the hit polygon's
118
+ * index, exposed via `polygon` (set when the underlying DOM target
119
+ * is an `<i>` polygon element).
120
+ */
121
+
122
+ /**
123
+ * Imperative handle exposed by `<PolyMesh ref>`. Read-only view of the
124
+ * mesh's element + current transform + polygons. Mutation flows through
125
+ * controlled props (parent owns transform state) — matches three.js
126
+ * editor's pattern of keeping `selected` external to the object.
127
+ */
128
+ interface PolyMeshHandle {
129
+ /** The `.polycss-mesh` wrapper div (null until mounted). */
130
+ readonly element: HTMLDivElement | null;
131
+ /** Identifier passed via the `id` prop, if any. */
132
+ readonly id?: string;
133
+ /** Current `position` prop value. */
134
+ getPosition(): Vec3 | undefined;
135
+ /** Current `rotation` prop value (Euler degrees). */
136
+ getRotation(): Vec3 | undefined;
137
+ /** Current `scale` prop value. */
138
+ getScale(): number | Vec3 | undefined;
139
+ /** Polygons currently being rendered (post-autoCenter). */
140
+ getPolygons(): Polygon[];
141
+ /**
142
+ * Replace the mesh polygons imperatively. Animated solid-triangle meshes use
143
+ * a stable DOM update path that mutates mounted leaves directly; unsupported
144
+ * topology falls back to the normal framework render path.
145
+ */
146
+ setPolygons(polygons: Polygon[]): void;
147
+ /**
148
+ * Snapshot the current `rotation` prop as the new "baked rotation" and
149
+ * trigger an atlas re-rasterization with the directional light
150
+ * inverse-rotated into the mesh's local frame.
151
+ *
152
+ * Call this after a rotate-mode drag ends (i.e. on pointer release) —
153
+ * **not** on every pointermove during the drag. The visual wrapper already
154
+ * follows the live `rotation` prop smoothly; the atlas only needs to
155
+ * update once per committed rotation so it doesn't re-bake every frame.
156
+ *
157
+ * Math rationale: baked atlas tiles encode `baseColor × Lambert(worldNormal,
158
+ * worldLight)`. When the mesh wrapper rotates via CSS the world-space normal
159
+ * changes but the baked color does not, causing stale shading. Calling
160
+ * `rebakeAtlas()` inverse-rotates the world light into the mesh-local frame
161
+ * before re-running the atlas baker, so `dot(localNormal, localLight) ===
162
+ * dot(worldNormal, worldLight)` and the shading is correct again.
163
+ *
164
+ * In dynamic (`textureLighting="dynamic"`) mode this call is a no-op for
165
+ * shading purposes (dynamic mode re-evaluates per frame), but it is still
166
+ * safe to call.
167
+ */
168
+ rebakeAtlas(): void;
169
+ /** Resolves when this mesh's current texture generation has usable backgrounds. */
170
+ whenTexturesReady(): Promise<void>;
171
+ /**
172
+ * Mutate a single polygon in place and re-render the mesh. `target` is
173
+ * either a polygon reference (as returned by `getPolygons()`) or its index.
174
+ * `partial` fields are merged onto the polygon via `Object.assign`. Skips
175
+ * the merge pass — cheaper than a full prop change for targeted edits like
176
+ * color picker updates from an inspector UI. Silently no-ops when `target`
177
+ * is not found or the index is out of range.
178
+ */
179
+ updatePolygon(target: Polygon | number, partial: Partial<Polygon>): void;
180
+ }
181
+ /**
182
+ * Pointer event payload delivered to <PolyMesh> handlers. Mirrors r3f's
183
+ * shape, minus raycaster-specific fields. See module docstring for the
184
+ * intentional divergences.
185
+ */
186
+ interface PolyPointerEvent<E extends Event = PointerEvent> {
187
+ /** The mesh originally under the pointer (deepest hit). */
188
+ object: PolyMeshHandle;
189
+ /** The mesh whose handler is being invoked. Equal to `object` until
190
+ * ancestor bubbling is added (out of scope for v1). */
191
+ eventObject: PolyMeshHandle;
192
+ /** All meshes stacked under the pointer this moment, front-to-back.
193
+ * Computed via `document.elementsFromPoint` then filtered to
194
+ * registered `.polycss-mesh` ancestors. */
195
+ intersections: Array<{
196
+ object: PolyMeshHandle;
197
+ }>;
198
+ /** Pointer position in normalized device coords [-1, 1] relative to
199
+ * the camera viewport. (0,0) = viewport center. Falls back to (0,0)
200
+ * when the mesh is rendered outside a `<PolyCamera>`. */
201
+ pointer: {
202
+ x: number;
203
+ y: number;
204
+ };
205
+ /** Pixel distance from the most recent `pointerdown` to this event.
206
+ * 0 on pointerdown itself. Use to discriminate click-vs-drag. */
207
+ delta: number;
208
+ /** The underlying DOM event. */
209
+ nativeEvent: E;
210
+ /** Stops native bubbling. (Equivalent to `nativeEvent.stopPropagation()`
211
+ * today; reserved for future r3f-style bubbling above the wrapper.) */
212
+ stopPropagation(): void;
213
+ }
214
+ type PolyMouseEvent = PolyPointerEvent<MouseEvent>;
215
+ type PolyWheelEvent = PolyPointerEvent<WheelEvent>;
216
+ type PolyEventHandler<E extends Event = PointerEvent> = (event: PolyPointerEvent<E>) => void;
217
+ /**
218
+ * Pointer / mouse / wheel handlers accepted by `<PolyMesh>`. Names mirror
219
+ * r3f exactly. Provide any handler to opt the mesh into receiving events;
220
+ * absent handlers add zero overhead.
221
+ */
222
+ interface InteractionProps {
223
+ onClick?: PolyEventHandler<MouseEvent>;
224
+ onContextMenu?: PolyEventHandler<MouseEvent>;
225
+ onDoubleClick?: PolyEventHandler<MouseEvent>;
226
+ onWheel?: PolyEventHandler<WheelEvent>;
227
+ onPointerDown?: PolyEventHandler<PointerEvent>;
228
+ onPointerUp?: PolyEventHandler<PointerEvent>;
229
+ onPointerMove?: PolyEventHandler<PointerEvent>;
230
+ onPointerOver?: PolyEventHandler<PointerEvent>;
231
+ onPointerOut?: PolyEventHandler<PointerEvent>;
232
+ onPointerEnter?: PolyEventHandler<PointerEvent>;
233
+ onPointerLeave?: PolyEventHandler<PointerEvent>;
234
+ onPointerCancel?: PolyEventHandler<PointerEvent>;
235
+ }
236
+ /** Walk up from `el` looking for the nearest registered mesh wrapper. */
237
+ declare function findPolyMeshHandle(el: Element | null): PolyMeshHandle | null;
238
+ /** Test whether `(clientX, clientY)` falls inside any polygon leaf
239
+ * child of `meshEl`'s post-3D bounding rect. Skips zero-area rects
240
+ * (happy-dom and pre-layout SSR return those). */
241
+ declare function pointInMeshElement(meshEl: HTMLElement, clientX: number, clientY: number): boolean;
242
+ /** Walk every registered `.polycss-mesh` in the document and return
243
+ * the first whose polygon bounding-rects contain `(clientX, clientY)`.
244
+ * An optional `filter` skips matched mesh elements (e.g. gizmos). */
245
+ declare function findMeshUnderPoint(clientX: number, clientY: number, filter?: (meshEl: HTMLElement) => boolean): PolyMeshHandle | null;
246
+
247
+ interface PolyMeshProps extends TransformProps, InteractionProps {
248
+ /** Stable identifier — exposed on the mesh handle and reflected as
249
+ * `data-poly-mesh-id` on the wrapper div. Use for selection lookups. */
250
+ id?: string;
251
+ /** URL to .obj / .glb / .gltf. Mutually exclusive with `polygons`. */
252
+ src?: string;
253
+ /**
254
+ * Companion `.mtl` URL for OBJ models. When set, materials defined in
255
+ * the mtl (Kd colors, map_Kd textures) are applied to the loaded mesh.
256
+ * Ignored for GLB/GLTF (they carry materials inline).
257
+ */
258
+ mtl?: string;
259
+ /** Pre-parsed polygons. Mutually exclusive with `src`. */
260
+ polygons?: Polygon[];
261
+ /** Optional `parseResult.voxelSource` companion for `.vox` meshes. When
262
+ * set alongside `polygons`, the direct voxel renderer fast path activates
263
+ * — emitting one `<b>` per visible voxel quad inside `.polycss-voxel-face`
264
+ * wrappers (matches vanilla's `scene.add(parseResult)` behaviour).
265
+ * Callers fetching via core's `loadMesh()` pass `parseResult.voxelSource`
266
+ * here so the fast path engages; callers fetching via PolyMesh's `src`
267
+ * prop get the same data wired through `useMesh` automatically. */
268
+ voxelSource?: _layoutit_polycss_core.ParseResult["voxelSource"];
269
+ /** Translate so mesh's bbox center is at local origin before applying `position`. */
270
+ autoCenter?: boolean;
271
+ /** Textured polygon lighting mode. Defaults to "baked". */
272
+ textureLighting?: PolyTextureLightingMode;
273
+ /** Atlas bitmap budget and CSS sprite size. `"auto"` (default) uses a
274
+ * device-appropriate memory budget (~4 MB mobile / ~16 MB desktop) and
275
+ * desktop/mobile sprite sizing. Numeric values 0.1..1 force an explicit
276
+ * raster scale and the 64px sprite. */
277
+ textureQuality?: TextureQuality;
278
+ /** Atlas leaf CSS primitive sizing. Defaults to scene context, then canonical. */
279
+ textureLeafSizing?: PolyTextureLeafSizing;
280
+ /** Default image filtering for atlas and direct image texture leaves. */
281
+ textureImageRendering?: PolyTextureImageRendering;
282
+ /** Default texture backend request. Defaults to scene context, then "auto". */
283
+ textureBackend?: PolyTextureBackend;
284
+ /** Default texture projection request. Defaults to scene context, then "affine". */
285
+ textureProjection?: PolyTextureProjection;
286
+ /** Solid seam overscan. `"auto"` computes a fitted per-edge amount from the polygon plan. */
287
+ seamBleed?: PolySeamBleed;
288
+ /**
289
+ * Hold the whole previous frame (geometry + texture) until the next atlas is
290
+ * decoded, then swap atomically — so a geometry edit never shows geometry
291
+ * before its texture. Best when edits arrive as discrete commits (no
292
+ * continuous drag). Defaults to false (bitmap streams in over live geometry).
293
+ */
294
+ atomicAtlas?: boolean;
295
+ /** Fires when the displayed atlas frame swaps to a ready one (atomic mode). */
296
+ onFrameReady?: () => void;
297
+ /** Per-polygon override render, or static children mounted inside the mesh wrapper. */
298
+ children?: ((polygon: Polygon, index: number) => ReactNode) | ReactNode;
299
+ /** Loading slot — rendered while `src` is being fetched/parsed. */
300
+ fallback?: ReactNode;
301
+ /** Error slot — rendered if parse fails. Receives the Error. */
302
+ errorFallback?: (error: Error) => ReactNode;
303
+ /** Parser options forwarded to parseObj/parseGltf. */
304
+ parseOptions?: UseMeshOptions;
305
+ /** Mesh optimization intent. Defaults to "lossy"; set "lossless" to keep
306
+ * authored surface fidelity. Top-level prop wins over `parseOptions.meshResolution`
307
+ * when both are present. */
308
+ meshResolution?: MeshResolution;
309
+ /**
310
+ * When `true`, emits a per-mesh SVG shadow path in both lighting modes.
311
+ * Each casting polygon projects onto the scene ground plane along the
312
+ * directional light; overlapping outlines are merged into one silhouette.
313
+ * Defaults to `false`.
314
+ */
315
+ castShadow?: boolean;
316
+ /**
317
+ * When `true`, this mesh acts as a shadow receiver. The scene's caster
318
+ * meshes (those with `castShadow=true`) project per-coplanar-face SVG
319
+ * shadows onto each visible surface of this mesh, matching Three.js's
320
+ * `mesh.receiveShadow` semantics. Disables the ground-shadow fallback
321
+ * for caster meshes — receivers handle shadow display. Defaults to `false`.
322
+ */
323
+ receiveShadow?: boolean;
324
+ /**
325
+ * Per-mesh parametric-shadow detail, overriding the scene's
326
+ * `shadow.definition` for this mesh's cast/self shadow. Only used when the
327
+ * scene's `shadow.parametric` is true. Unset → inherit the scene definition.
328
+ */
329
+ shadowDefinition?: number;
330
+ /**
331
+ * Apply mesh optimization (coplanar merge + interior cull) before
332
+ * rendering. Defaults to `true` — matches vanilla `scene.add`'s default.
333
+ * Set `false` for helper meshes (axes, light markers) whose geometry
334
+ * shouldn't be merged, or when the imperative `updatePolygon` API needs
335
+ * polygon refs to survive across updates.
336
+ */
337
+ merge?: boolean;
338
+ className?: string;
339
+ style?: CSSProperties;
340
+ }
341
+ declare const PolyMesh: React.ForwardRefExoticComponent<PolyMeshProps & React.RefAttributes<PolyMeshHandle>>;
342
+
343
+ export { type DOMPassthroughProps as D, type InteractionProps as I, type PolyProps as P, type TransformProps as T, type UseMeshOptions as U, type PolyMeshProps as a, type PolyMeshHandle as b, type PolyEventHandler as c, PolyMesh as d, type PolyMouseEvent as e, type PolyPointerEvent as f, type PolyWheelEvent as g, type UseMeshResult as h, findMeshUnderPoint as i, findPolyMeshHandle as j, pointInMeshElement as p, usePolyMesh as u };
package/dist/index.cjs CHANGED
@@ -2748,6 +2748,7 @@ function PolySceneInner({
2748
2748
  rotY: _rotY,
2749
2749
  zoom: _zoom,
2750
2750
  directionalLight,
2751
+ pointLights,
2751
2752
  ambientLight,
2752
2753
  textureLighting = "baked",
2753
2754
  textureQuality,
@@ -2805,17 +2806,19 @@ function PolySceneInner({
2805
2806
  const sceneBbox = centerInputPolygons ? centerSceneBbox : renderSceneBbox;
2806
2807
  const directionalForAtlas = textureLighting === "dynamic" ? void 0 : directionalLight;
2807
2808
  const ambientForAtlas = textureLighting === "dynamic" ? void 0 : ambientLight;
2809
+ const pointLightsForAtlas = textureLighting === "dynamic" ? void 0 : pointLights;
2808
2810
  const polyContext = (0, import_react14.useMemo)(() => {
2809
2811
  const tileSize = 50;
2810
2812
  return {
2811
2813
  tileSize,
2812
2814
  layerElevation: tileSize,
2813
2815
  directionalLight: directionalForAtlas,
2816
+ pointLights: pointLightsForAtlas,
2814
2817
  ambientLight: ambientForAtlas,
2815
2818
  textureLighting,
2816
2819
  seamBleed
2817
2820
  };
2818
- }, [directionalForAtlas, ambientForAtlas, textureLighting, seamBleed]);
2821
+ }, [directionalForAtlas, pointLightsForAtlas, ambientForAtlas, textureLighting, seamBleed]);
2819
2822
  const autoCenterOffset = (0, import_react14.useMemo)(() => {
2820
2823
  if (!autoCenter) return [0, 0, 0];
2821
2824
  return [
@@ -3001,6 +3004,7 @@ function PolySceneInner({
3001
3004
  () => ({
3002
3005
  textureLighting,
3003
3006
  directionalLight,
3007
+ pointLights,
3004
3008
  ambientLight,
3005
3009
  strategies,
3006
3010
  seamBleed,
@@ -3017,7 +3021,7 @@ function PolySceneInner({
3017
3021
  groundCssZ,
3018
3022
  sceneEl
3019
3023
  }),
3020
- [textureLighting, directionalLight, ambientLight, strategies, seamBleed, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, shadow, registerShadowCaster, registerShadowReceiver, shadowCastersVersion, hasShadowReceiver, groundCssZ, sceneEl]
3024
+ [textureLighting, directionalLight, pointLights, ambientLight, strategies, seamBleed, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, shadow, registerShadowCaster, registerShadowReceiver, shadowCastersVersion, hasShadowReceiver, groundCssZ, sceneEl]
3021
3025
  );
3022
3026
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3023
3027
  "div",
@@ -3907,6 +3911,7 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
3907
3911
  onFrameReady,
3908
3912
  castShadow,
3909
3913
  receiveShadow,
3914
+ shadowDefinition,
3910
3915
  merge = true,
3911
3916
  children,
3912
3917
  fallback,
@@ -4175,6 +4180,18 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4175
4180
  direction: (0, import_polycss_core16.inverseRotateVec3)(cssLight.direction, rot)
4176
4181
  };
4177
4182
  }, [effectiveDirectional, bakedRotation]);
4183
+ const bakedPointLights = (0, import_react16.useMemo)(() => {
4184
+ const pls = sceneCtx?.pointLights;
4185
+ if (!pls || pls.length === 0) return void 0;
4186
+ const pos = position ?? [0, 0, 0];
4187
+ const rot = bakedRotation ?? [0, 0, 0];
4188
+ const hasRot = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;
4189
+ return pls.map((pl) => {
4190
+ const rel = [pl.position[0] - pos[0], pl.position[1] - pos[1], pl.position[2] - pos[2]];
4191
+ const local = hasRot ? (0, import_polycss_core16.inverseRotateVec3)(rel, rot) : rel;
4192
+ return { ...pl, position: local };
4193
+ });
4194
+ }, [sceneCtx?.pointLights, position, bakedRotation]);
4178
4195
  const lightOccludedPolyIndices = void 0;
4179
4196
  const atlasPlans = (0, import_react16.useMemo)(
4180
4197
  () => {
@@ -4193,6 +4210,7 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4193
4210
  i,
4194
4211
  {
4195
4212
  directionalLight: bakedDirectional,
4213
+ pointLights: bakedPointLights,
4196
4214
  ambientLight: effectiveAmbient,
4197
4215
  seamBleed: seamBleedEdges?.has(i) ? effectiveSeamBleed : void 0,
4198
4216
  seamEdges: seamBleedEdges?.get(i),
@@ -4202,7 +4220,7 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4202
4220
  basisHints[i]
4203
4221
  ));
4204
4222
  },
4205
- [renderPolygon, directVoxelEnabled, polygons, bakedDirectional, effectiveAmbient, effectiveSeamBleed, lightOccludedPolyIndices]
4223
+ [renderPolygon, directVoxelEnabled, polygons, bakedDirectional, bakedPointLights, effectiveAmbient, effectiveSeamBleed, lightOccludedPolyIndices]
4206
4224
  );
4207
4225
  const textureAtlas = useTextureAtlas(
4208
4226
  atlasPlans,
@@ -4248,23 +4266,32 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4248
4266
  }
4249
4267
  return s;
4250
4268
  }, [atlasPlans, polygons]);
4269
+ const shadowCasterRegisteredRef = (0, import_react16.useRef)(false);
4270
+ const lastShadowPolyCountRef = (0, import_react16.useRef)(-1);
4251
4271
  (0, import_react16.useEffect)(() => {
4252
- if (!sceneRegisterShadowCaster) return;
4253
- if (castShadow) {
4254
- sceneRegisterShadowCaster(meshIdRef.current, {
4255
- polygons,
4256
- position: position ?? [0, 0, 0],
4257
- scale,
4258
- rotation,
4259
- renderedPolygonIndices
4260
- });
4261
- } else {
4262
- sceneRegisterShadowCaster(meshIdRef.current, null);
4263
- }
4272
+ if (!sceneRegisterShadowCaster || !castShadow) return;
4264
4273
  return () => {
4265
4274
  sceneRegisterShadowCaster(meshIdRef.current, null);
4275
+ shadowCasterRegisteredRef.current = false;
4276
+ lastShadowPolyCountRef.current = -1;
4266
4277
  };
4267
- }, [sceneRegisterShadowCaster, castShadow, polygons, position, scale, rotation, renderedPolygonIndices]);
4278
+ }, [sceneRegisterShadowCaster, castShadow]);
4279
+ (0, import_react16.useEffect)(() => {
4280
+ if (!sceneRegisterShadowCaster || !castShadow) return;
4281
+ const followAnimation = sceneCtx?.shadow?.followAnimation ?? false;
4282
+ const topologyChanged = polygons.length !== lastShadowPolyCountRef.current;
4283
+ if (shadowCasterRegisteredRef.current && !followAnimation && !topologyChanged) return;
4284
+ lastShadowPolyCountRef.current = polygons.length;
4285
+ shadowCasterRegisteredRef.current = true;
4286
+ sceneRegisterShadowCaster(meshIdRef.current, {
4287
+ polygons,
4288
+ position: position ?? [0, 0, 0],
4289
+ scale,
4290
+ rotation,
4291
+ renderedPolygonIndices,
4292
+ shadowDefinition
4293
+ });
4294
+ }, [sceneRegisterShadowCaster, castShadow, polygons, position, scale, rotation, renderedPolygonIndices, shadowDefinition, sceneCtx?.shadow]);
4268
4295
  const sceneRegisterShadowReceiver = sceneCtx?.registerShadowReceiver;
4269
4296
  (0, import_react16.useEffect)(() => {
4270
4297
  if (!sceneRegisterShadowReceiver) return;
@@ -4390,6 +4417,16 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4390
4417
  if (!shadowCasters || shadowCasters.size === 0) return null;
4391
4418
  const userLightDir = sceneDirectionalLight?.direction ?? [0.4, -0.7, 0.59];
4392
4419
  const lightDir = (0, import_polycss_core16.worldDirectionToCss)(userLightDir);
4420
+ const dynamicShading = effectiveTextureLighting === "dynamic";
4421
+ const scenePoints = dynamicShading ? [] : sceneCtx?.pointLights ?? [];
4422
+ const allPointLightsCss = scenePoints.map((pl) => ({
4423
+ position: (0, import_polycss_core16.worldPositionToCss)(pl.position),
4424
+ color: pl.color,
4425
+ intensity: pl.intensity
4426
+ }));
4427
+ const shadowPointIndices = scenePoints.map((pl, i) => pl.castShadow ? i : -1).filter((i) => i >= 0);
4428
+ const runDirectionalShadow = !!sceneDirectionalLight?.direction && (sceneDirectionalLight.intensity ?? 1) > 0;
4429
+ const hasShadowPoints = shadowPointIndices.length > 0;
4393
4430
  const shadowLift = sceneShadow?.lift ?? 1e-3;
4394
4431
  const planes = (0, import_polycss_core16.prepareReceiverFacePlanes)(
4395
4432
  polygons,
@@ -4402,18 +4439,17 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4402
4439
  if (planes.length === 0) return null;
4403
4440
  const casterInputs = [];
4404
4441
  for (const [casterId, data] of shadowCasters) {
4405
- const rendered = data.renderedPolygonIndices;
4406
4442
  const items = (0, import_polycss_core16.prepareCasterPolyItems)(
4407
4443
  data.polygons,
4408
4444
  data.position,
4409
4445
  data.scale,
4410
- rendered ? (idx) => rendered.has(idx) : () => true,
4446
+ () => true,
4411
4447
  data.rotation ?? null
4412
4448
  );
4413
4449
  const isSelf = data.polygons === polygons;
4414
4450
  const selfMap = isSelf ? selfShadowEdgeMap : void 0;
4415
4451
  let edgeOwners;
4416
- if (!isSelf && data.polygons.length >= 40) {
4452
+ if (!isSelf && (data.polygons.length >= 40 || hasShadowPoints)) {
4417
4453
  const dposArr = data.position;
4418
4454
  const drot = data.rotation ?? null;
4419
4455
  const dsKey = JSON.stringify(data.scale ?? null);
@@ -4426,12 +4462,29 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4426
4462
  }
4427
4463
  edgeOwners = cachedOwners;
4428
4464
  }
4465
+ let overrideSilhouette;
4466
+ let overridePointSilhouettes;
4467
+ if (sceneShadow?.parametric) {
4468
+ const def = data.shadowDefinition ?? sceneShadow.definition ?? 16;
4469
+ const result = (0, import_polycss_core16.buildParametricCasterOverride)({
4470
+ polysWorldVerts: items.map((it) => it.wv),
4471
+ lightDir,
4472
+ definition: def,
4473
+ isSelf,
4474
+ style: sceneShadow.style,
4475
+ pointLights: shadowPointIndices.map((i) => ({ position: allPointLightsCss[i].position, index: i }))
4476
+ });
4477
+ overrideSilhouette = result.overrideSilhouette;
4478
+ overridePointSilhouettes = result.overridePointSilhouettes;
4479
+ }
4429
4480
  casterInputs.push({
4430
4481
  id: casterId,
4431
4482
  items,
4432
4483
  selfShadowEdgeMap: selfMap,
4433
4484
  edgeOwners,
4434
- casterPolygonCount: data.polygons.length
4485
+ casterPolygonCount: data.polygons.length,
4486
+ overrideSilhouette,
4487
+ overridePointSilhouettes
4435
4488
  });
4436
4489
  }
4437
4490
  const cameraState = cameraCtx?.store.getState().cameraState;
@@ -4440,27 +4493,31 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4440
4493
  rotY: cameraState?.rotY ?? 45,
4441
4494
  meshRotation: rotation
4442
4495
  };
4443
- const specs = (0, import_polycss_core16.computeReceiverShadowFaces)({
4496
+ const faces = (0, import_polycss_core16.computeMergedReceiverShadows)({
4444
4497
  receiverPlanes: planes,
4445
4498
  receiverPolygons: polygons,
4446
4499
  receiverHasTexture: polygons.some((p) => p.texture !== void 0),
4447
4500
  casters: casterInputs,
4448
4501
  lightDir,
4502
+ runDirectional: runDirectionalShadow,
4503
+ pointPasses: shadowPointIndices.map((i) => ({ lightPos: allPointLightsCss[i].position, index: i })),
4504
+ allPointLights: allPointLightsCss,
4449
4505
  cameraRot,
4450
4506
  ambientLight: sceneCtx?.ambientLight,
4451
4507
  directionalLight: sceneDirectionalLight,
4452
4508
  shadow: { color: sceneShadow?.color, opacity: sceneShadow?.opacity ?? 0.25, maxExtend: sceneShadow?.maxExtend }
4453
4509
  });
4454
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_jsx_runtime9.Fragment, { children: specs.map((spec) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4510
+ if (faces.length === 0) return null;
4511
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_jsx_runtime9.Fragment, { children: faces.map((fc) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4455
4512
  "svg",
4456
4513
  {
4457
4514
  className: "polycss-shadow polycss-shadow-svg polycss-shadow-receiver",
4458
4515
  "data-poly-shadow-type": "receiver",
4459
- "data-poly-shadow-receiver-face": spec.faceIndex,
4460
- "data-poly-shadow-receiver-polys": JSON.stringify(spec.memberPolyIndices),
4461
- width: spec.width,
4462
- height: spec.height,
4463
- viewBox: `0 0 ${spec.width} ${spec.height}`,
4516
+ "data-poly-shadow-receiver-face": fc.faceIndex,
4517
+ "data-poly-shadow-receiver-polys": JSON.stringify(fc.memberPolyIndices),
4518
+ width: fc.width,
4519
+ height: fc.height,
4520
+ viewBox: `0 0 ${fc.width} ${fc.height}`,
4464
4521
  style: {
4465
4522
  position: "absolute",
4466
4523
  top: 0,
@@ -4470,25 +4527,27 @@ var PolyMesh = (0, import_react16.forwardRef)(function PolyMesh2({
4470
4527
  transformOrigin: "0 0",
4471
4528
  pointerEvents: "none",
4472
4529
  willChange: "transform",
4473
- transform: spec.matrixCss
4530
+ opacity: fc.svgOpacity,
4531
+ transform: fc.matrixCss
4474
4532
  },
4475
- children: spec.paths.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4476
- "path",
4477
- {
4478
- d: p.d,
4479
- fill: spec.fill,
4480
- stroke: spec.fill,
4481
- strokeWidth: "3",
4482
- strokeLinejoin: "round",
4483
- opacity: spec.opacity.toFixed(4),
4484
- "data-poly-shadow-caster-polys": JSON.stringify(p.casterPolygonIndices)
4485
- },
4486
- i
4487
- ))
4533
+ children: [
4534
+ fc.baseFill && fc.baseD ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: fc.baseD, fill: fc.baseFill, fillRule: "nonzero" }) : null,
4535
+ fc.layers.map((layer, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4536
+ "path",
4537
+ {
4538
+ d: layer.d,
4539
+ fill: layer.fill,
4540
+ fillRule: "nonzero",
4541
+ opacity: layer.opacity !== 1 ? layer.opacity.toFixed(4) : void 0,
4542
+ style: layer.multiply ? { mixBlendMode: "multiply" } : void 0
4543
+ },
4544
+ i
4545
+ ))
4546
+ ]
4488
4547
  },
4489
- `receiver-${spec.faceIndex}`
4548
+ `receiver-${fc.faceIndex}`
4490
4549
  )) });
4491
- }, [receiveShadow, shadowCasters, shadowCastersVersion, polygons, position, scale, rotation, sceneDirectionalLight, sceneShadow, sceneCtx?.ambientLight, cameraCtx?.store, cameraTick, selfShadowEdgeMap]);
4550
+ }, [receiveShadow, shadowCasters, shadowCastersVersion, polygons, position, scale, rotation, sceneDirectionalLight, sceneCtx?.pointLights, effectiveTextureLighting, sceneShadow, sceneCtx?.ambientLight, cameraCtx?.store, cameraTick, selfShadowEdgeMap]);
4492
4551
  const portalSceneEl = sceneCtx?.sceneEl ?? null;
4493
4552
  const portaledReceiverShadowSvgs = portalSceneEl && receiverShadowSvgs ? (0, import_react_dom.createPortal)(receiverShadowSvgs, portalSceneEl) : null;
4494
4553
  setPolygonsImplRef.current = (nextPolygons) => {