@layoutit/polycss-react 0.2.6 → 0.2.7

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/README.md CHANGED
@@ -69,7 +69,7 @@ export default function App() {
69
69
  ### PolyScene
70
70
 
71
71
  - `polygons` renders a static `Polygon[]` directly.
72
- - `directionalLight` and `ambientLight` control scene lighting.
72
+ - `directionalLight`, `pointLights` (direction-only, baked mode; optional per-light `castShadow`), and `ambientLight` control scene lighting.
73
73
  - `textureLighting` chooses `"baked"` or `"dynamic"`.
74
74
  - `textureQuality` controls atlas raster budget.
75
75
  - `strategies` can disable selected render strategies for diagnostics.
@@ -84,7 +84,6 @@ export default function App() {
84
84
  - `autoCenter` shifts the mesh bbox center to local origin.
85
85
  - `meshResolution` chooses `"lossy"` (default) or `"lossless"` optimization. STL imports use the conservative lossless path in both modes.
86
86
  - `castShadow` emits CSS-projected shadows in dynamic lighting mode.
87
- - Tooling can reuse `buildPolyMeshTransform`, `buildPolySceneTransform`, `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDirectionalLightToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, and `polyCssPositionToWorld` for renderer-compatible transforms and world/CSS conversions.
88
87
 
89
88
  ### Controls
90
89
 
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) => {
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import * as React$1 from 'react';
2
2
  import React__default, { ReactNode, CSSProperties, MouseEventHandler, PointerEventHandler, FocusEventHandler, KeyboardEventHandler, IframeHTMLAttributes, RefObject, MouseEvent as MouseEvent$1 } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as _layoutit_polycss_core from '@layoutit/polycss-core';
5
- import { Vec3, CameraState, CameraHandle, PolyCameraProjection, PolyTextureImageSource, PolyTexturePresentation, Vec2, PolyMaterial, PolyDirectionalLight, PolyTextureLightingMode, TextureQuality, PolyTextureLeafSizing, PolyTextureImageRendering, PolyTextureBackend, PolyTextureProjection, Polygon, PolyAmbientLight, PolySeamBleed, PolyRenderStrategiesOption, LoadMeshOptions, ParseResult, MeshResolution, BoxPolygonsOptions, ConePolygonsOptions, CylinderPolygonsOptions, DodecahedronPolygonsOptions, IcosahedronPolygonsOptions, OctahedronPolygonsOptions, PlanePolygonsOptions, RingPolygonsOptions, SpherePolygonsOptions, TetrahedronPolygonsOptions, TorusPolygonsOptions, PolyAnimationTarget, PolyAnimationMixer, PolyAnimationClip, PolyAnimationAction, ParseAnimationController } from '@layoutit/polycss-core';
5
+ import { Vec3, CameraState, CameraHandle, PolyCameraProjection, PolyTextureImageSource, PolyTexturePresentation, Vec2, PolyMaterial, PolyDirectionalLight, PolyTextureLightingMode, TextureQuality, PolyTextureLeafSizing, PolyTextureImageRendering, PolyTextureBackend, PolyTextureProjection, Polygon, PolyPointLight, PolyAmbientLight, PolySeamBleed, PolyRenderStrategiesOption, LoadMeshOptions, ParseResult, MeshResolution, BoxPolygonsOptions, ConePolygonsOptions, CylinderPolygonsOptions, DodecahedronPolygonsOptions, IcosahedronPolygonsOptions, OctahedronPolygonsOptions, PlanePolygonsOptions, RingPolygonsOptions, SpherePolygonsOptions, TetrahedronPolygonsOptions, TorusPolygonsOptions, PolyAnimationTarget, PolyAnimationMixer, PolyAnimationClip, PolyAnimationAction, ParseAnimationController } from '@layoutit/polycss-core';
6
6
  export { ArrowPolygonsOptions, AutoRotateConfig, AutoRotateOption, AxesHelperOptions, BASE_TILE, BoxFace, BoxFaceOptions, BoxPolygonsOptions, CAMERA_BACKFACE_CULL_EPS, CameraCullNormalGroup, CameraCullRotation, CameraHandle, CameraState, CameraStyleInput, ConePolygonsOptions, CoverPlanarPolygonsOptions, CullInteriorOptions, CylinderPolygonsOptions, DEFAULT_CAMERA_STATE, DEFAULT_PROJECTION, DEFAULT_SEAM_FACET_SPLIT_OPTIONS, DEFAULT_SEAM_OVERLAP_OPTIONS, DodecahedronPolygonsOptions, GltfParseOptions, IcosahedronPolygonsOptions, LoadMeshOptions, LoopMode, LoopOnce, LoopPingPong, LoopRepeat, MeshResolution, MtlParseResult, NormalizeResult, ObjParseOptions, OctahedronPolygonsOptions, OptimizeAnimatedMeshPolygonsOptions, OptimizeMeshParseResultOptions, OptimizeMeshPolygonsOptions, ParseAnimationClip, ParseAnimationController, ParseResult, ParseStlColor, ParseStlSolid, ParseStlTopology, ParsedColor, PlanePolygonsOptions, PolyAmbientLight, PolyAnimationAction, PolyAnimationClip, PolyAnimationMixer, PolyAnimationTarget, PolyCameraProjection, PolyCameraSnapshot, PolyCameraSnapshotOptions, PolyCameraSnapshotSource, PolyDirectionalLight, PolyMaterial, PolyMeshTransformInput, PolyRenderStrategiesOption, PolyRenderStrategy, PolySceneTransformInput, PolyTextureBackend, PolyTextureImageLighting, PolyTextureImageRendering, PolyTextureImageSource, PolyTextureLeafSizing, PolyTextureLightingMode, PolyTexturePresentation, PolyTextureProjection, Polygon, PolygonFace, RingPolygonsOptions, SceneBbox, SceneContext, SceneContextBuildArgs, SceneContextBuildResult, SeamFacetSplitCandidate, SeamFacetSplitCandidateReason, SeamFacetSplitOptions, SeamFacetSplitReport, SeamOverlapCandidate, SeamOverlapCandidateKind, SeamOverlapDiagnostics, SeamOverlapOptions, SimplifyTriangleMeshPolygonsOptions, SolidTextureSampleOptions, StlParseOptions, TetrahedronPolygonsOptions, TexturePaintMetrics, TexturePaintMetricsOptions, TextureTriangle, TorusPolygonsOptions, VOXEL_CAMERA_CULL_AXIS_EPS, VOXEL_CAMERA_CULL_NORMAL_LIMIT, Vec2, Vec3, VoxParseOptions, arrowPolygons, axesHelperPolygons, bakeSolidTextureSampledPolygons, bakeSolidTextureSamples, boxPolygons, buildPolyCameraSceneTransform, buildPolyMeshTransform, buildPolySceneTransform, buildSceneContext, cameraCullNormalGroups, cameraCullNormalGroupsFromPolygons, cameraCullNormalKey, cameraCullVisibleSignature, cameraFacingDepth, capturePolyCameraSnapshot, clampChannel, computeSceneBbox, computeShapeLighting, computeTexturePaintMetrics, conePolygons, coverPlanarPolygons, createIsometricCamera, createPolyAnimationMixer, cssDistanceToWorld, cssPositionToWorld, cullInteriorPolygons, cylinderPolygons, dodecahedronPolygons, formatColor, icosahedronPolygons, inverseRotateVec3, isAxisAlignedSurfaceNormal, isVoxelCameraCullableNormalGroups, loadMesh, mergePolygons, normalFacesCamera, normalizeInvertMultiplier, normalizePolygons, octahedronPolygons, optimizeAnimatedMeshPolygons, optimizeMeshParseResult, optimizeMeshPolygons, parseColor, parseGltf, parseHexColor, parseMtl, parseObj, parsePureColor, parseRgbColor, parseStl, parseVox, planePolygons, polyCameraTargetToCss, polyCssDistanceToWorld, polyCssPositionToWorld, polygonCssSurfaceNormal, polygonFaces, polygonFacesCamera, repairMeshSeams, resolvePolyCameraAppliedPerspectiveStyle, ringPolygons, rotateVec3, seamFacetSplitPolygons, seamFacetSplitReport, seamOverlapDiagnostics, seamOverlapPolygons, seamOverlapReport, shadeColor, simplifyTriangleMeshPolygons, tetrahedronPolygons, torusPolygons, worldDirectionToCss, worldDirectionToPolyCss, worldDirectionalLightToCss, worldDirectionalLightToPolyCss, worldDistanceToCss, worldDistanceToPolyCss, worldPositionToCss, worldPositionToPolyCss } from '@layoutit/polycss-core';
7
7
 
8
8
  interface PolyPerspectiveCameraProps {
@@ -118,6 +118,32 @@ interface ShadowOptions {
118
118
  * disable the cap entirely.
119
119
  */
120
120
  maxExtend?: number;
121
+ /**
122
+ * Cast a low-resolution parametric silhouette per caster instead of its full
123
+ * geometry — lighter DOM + cheaper projection at the cost of an approximate
124
+ * outline. Casts onto every receiver through the normal pipeline. Default:
125
+ * `false`. (Directional + point lights; the exact path stays the default.)
126
+ */
127
+ parametric?: boolean;
128
+ /**
129
+ * Parametric-shadow detail (max silhouette points / pixel-grid resolution).
130
+ * Only used when `parametric` is true. Default: `16`. Override per mesh via
131
+ * `<PolyMesh shadowDefinition>`.
132
+ */
133
+ definition?: number;
134
+ /**
135
+ * Parametric render style: `"vector"` (smooth contour, default) or `"pixel"`
136
+ * (greedy-meshed voxel blocks; `definition` becomes the grid resolution).
137
+ */
138
+ style?: "vector" | "pixel";
139
+ /**
140
+ * Re-emit a caster's shadow while it animates (deforms) so the shadow follows
141
+ * the pose instead of freezing. Default `false`: a same-topology deform keeps
142
+ * the last shadow pose (re-emitting every frame is expensive). Best with
143
+ * `parametric` — a low-res silhouette is cheap to reproject. Topology changes
144
+ * (different polygon count) always re-emit regardless.
145
+ */
146
+ followAnimation?: boolean;
121
147
  }
122
148
 
123
149
  /** Three.js-style transform props accepted by every PolyCSS component. */
@@ -227,6 +253,9 @@ interface PolySceneProps extends TransformProps {
227
253
  rotY?: number;
228
254
  zoom?: number;
229
255
  directionalLight?: PolyDirectionalLight;
256
+ /** Point lights (world-space positions). Direction-only per-face Lambert,
257
+ * baked-mode only. */
258
+ pointLights?: PolyPointLight[];
230
259
  ambientLight?: PolyAmbientLight;
231
260
  /** Textured polygon lighting mode. Defaults to "baked". */
232
261
  textureLighting?: PolyTextureLightingMode;
@@ -275,7 +304,7 @@ interface PolySceneProps extends TransformProps {
275
304
  debugShowLabels?: boolean;
276
305
  debugShowBackfaces?: boolean;
277
306
  }
278
- declare function PolySceneInner({ polygons: polygonsProp, centerPolygons: centerPolygonsProp, perspective: _perspective, rotX: _rotX, rotY: _rotY, zoom: _zoom, directionalLight, ambientLight, textureLighting, textureQuality, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, seamBleed, strategies, autoCenter, shadow, className, style, children, position: _position, scale: _scale, rotation: _rotation, debugShowLabels: _debugShowLabels, debugShowBackfaces, }: PolySceneProps): react_jsx_runtime.JSX.Element;
307
+ declare function PolySceneInner({ polygons: polygonsProp, centerPolygons: centerPolygonsProp, perspective: _perspective, rotX: _rotX, rotY: _rotY, zoom: _zoom, directionalLight, pointLights, ambientLight, textureLighting, textureQuality, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, seamBleed, strategies, autoCenter, shadow, className, style, children, position: _position, scale: _scale, rotation: _rotation, debugShowLabels: _debugShowLabels, debugShowBackfaces, }: PolySceneProps): react_jsx_runtime.JSX.Element;
279
308
  declare const PolyScene: React$1.MemoExoticComponent<typeof PolySceneInner>;
280
309
 
281
310
  type UseMeshOptions = LoadMeshOptions;
@@ -511,6 +540,12 @@ interface PolyMeshProps extends TransformProps, InteractionProps {
511
540
  * for caster meshes — receivers handle shadow display. Defaults to `false`.
512
541
  */
513
542
  receiveShadow?: boolean;
543
+ /**
544
+ * Per-mesh parametric-shadow detail, overriding the scene's
545
+ * `shadow.definition` for this mesh's cast/self shadow. Only used when the
546
+ * scene's `shadow.parametric` is true. Unset → inherit the scene definition.
547
+ */
548
+ shadowDefinition?: number;
514
549
  /**
515
550
  * Apply mesh optimization (coplanar merge + interior cull) before
516
551
  * rendering. Defaults to `true` — matches vanilla `scene.add`'s default.
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as React$1 from 'react';
2
2
  import React__default, { ReactNode, CSSProperties, MouseEventHandler, PointerEventHandler, FocusEventHandler, KeyboardEventHandler, IframeHTMLAttributes, RefObject, MouseEvent as MouseEvent$1 } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as _layoutit_polycss_core from '@layoutit/polycss-core';
5
- import { Vec3, CameraState, CameraHandle, PolyCameraProjection, PolyTextureImageSource, PolyTexturePresentation, Vec2, PolyMaterial, PolyDirectionalLight, PolyTextureLightingMode, TextureQuality, PolyTextureLeafSizing, PolyTextureImageRendering, PolyTextureBackend, PolyTextureProjection, Polygon, PolyAmbientLight, PolySeamBleed, PolyRenderStrategiesOption, LoadMeshOptions, ParseResult, MeshResolution, BoxPolygonsOptions, ConePolygonsOptions, CylinderPolygonsOptions, DodecahedronPolygonsOptions, IcosahedronPolygonsOptions, OctahedronPolygonsOptions, PlanePolygonsOptions, RingPolygonsOptions, SpherePolygonsOptions, TetrahedronPolygonsOptions, TorusPolygonsOptions, PolyAnimationTarget, PolyAnimationMixer, PolyAnimationClip, PolyAnimationAction, ParseAnimationController } from '@layoutit/polycss-core';
5
+ import { Vec3, CameraState, CameraHandle, PolyCameraProjection, PolyTextureImageSource, PolyTexturePresentation, Vec2, PolyMaterial, PolyDirectionalLight, PolyTextureLightingMode, TextureQuality, PolyTextureLeafSizing, PolyTextureImageRendering, PolyTextureBackend, PolyTextureProjection, Polygon, PolyPointLight, PolyAmbientLight, PolySeamBleed, PolyRenderStrategiesOption, LoadMeshOptions, ParseResult, MeshResolution, BoxPolygonsOptions, ConePolygonsOptions, CylinderPolygonsOptions, DodecahedronPolygonsOptions, IcosahedronPolygonsOptions, OctahedronPolygonsOptions, PlanePolygonsOptions, RingPolygonsOptions, SpherePolygonsOptions, TetrahedronPolygonsOptions, TorusPolygonsOptions, PolyAnimationTarget, PolyAnimationMixer, PolyAnimationClip, PolyAnimationAction, ParseAnimationController } from '@layoutit/polycss-core';
6
6
  export { ArrowPolygonsOptions, AutoRotateConfig, AutoRotateOption, AxesHelperOptions, BASE_TILE, BoxFace, BoxFaceOptions, BoxPolygonsOptions, CAMERA_BACKFACE_CULL_EPS, CameraCullNormalGroup, CameraCullRotation, CameraHandle, CameraState, CameraStyleInput, ConePolygonsOptions, CoverPlanarPolygonsOptions, CullInteriorOptions, CylinderPolygonsOptions, DEFAULT_CAMERA_STATE, DEFAULT_PROJECTION, DEFAULT_SEAM_FACET_SPLIT_OPTIONS, DEFAULT_SEAM_OVERLAP_OPTIONS, DodecahedronPolygonsOptions, GltfParseOptions, IcosahedronPolygonsOptions, LoadMeshOptions, LoopMode, LoopOnce, LoopPingPong, LoopRepeat, MeshResolution, MtlParseResult, NormalizeResult, ObjParseOptions, OctahedronPolygonsOptions, OptimizeAnimatedMeshPolygonsOptions, OptimizeMeshParseResultOptions, OptimizeMeshPolygonsOptions, ParseAnimationClip, ParseAnimationController, ParseResult, ParseStlColor, ParseStlSolid, ParseStlTopology, ParsedColor, PlanePolygonsOptions, PolyAmbientLight, PolyAnimationAction, PolyAnimationClip, PolyAnimationMixer, PolyAnimationTarget, PolyCameraProjection, PolyCameraSnapshot, PolyCameraSnapshotOptions, PolyCameraSnapshotSource, PolyDirectionalLight, PolyMaterial, PolyMeshTransformInput, PolyRenderStrategiesOption, PolyRenderStrategy, PolySceneTransformInput, PolyTextureBackend, PolyTextureImageLighting, PolyTextureImageRendering, PolyTextureImageSource, PolyTextureLeafSizing, PolyTextureLightingMode, PolyTexturePresentation, PolyTextureProjection, Polygon, PolygonFace, RingPolygonsOptions, SceneBbox, SceneContext, SceneContextBuildArgs, SceneContextBuildResult, SeamFacetSplitCandidate, SeamFacetSplitCandidateReason, SeamFacetSplitOptions, SeamFacetSplitReport, SeamOverlapCandidate, SeamOverlapCandidateKind, SeamOverlapDiagnostics, SeamOverlapOptions, SimplifyTriangleMeshPolygonsOptions, SolidTextureSampleOptions, StlParseOptions, TetrahedronPolygonsOptions, TexturePaintMetrics, TexturePaintMetricsOptions, TextureTriangle, TorusPolygonsOptions, VOXEL_CAMERA_CULL_AXIS_EPS, VOXEL_CAMERA_CULL_NORMAL_LIMIT, Vec2, Vec3, VoxParseOptions, arrowPolygons, axesHelperPolygons, bakeSolidTextureSampledPolygons, bakeSolidTextureSamples, boxPolygons, buildPolyCameraSceneTransform, buildPolyMeshTransform, buildPolySceneTransform, buildSceneContext, cameraCullNormalGroups, cameraCullNormalGroupsFromPolygons, cameraCullNormalKey, cameraCullVisibleSignature, cameraFacingDepth, capturePolyCameraSnapshot, clampChannel, computeSceneBbox, computeShapeLighting, computeTexturePaintMetrics, conePolygons, coverPlanarPolygons, createIsometricCamera, createPolyAnimationMixer, cssDistanceToWorld, cssPositionToWorld, cullInteriorPolygons, cylinderPolygons, dodecahedronPolygons, formatColor, icosahedronPolygons, inverseRotateVec3, isAxisAlignedSurfaceNormal, isVoxelCameraCullableNormalGroups, loadMesh, mergePolygons, normalFacesCamera, normalizeInvertMultiplier, normalizePolygons, octahedronPolygons, optimizeAnimatedMeshPolygons, optimizeMeshParseResult, optimizeMeshPolygons, parseColor, parseGltf, parseHexColor, parseMtl, parseObj, parsePureColor, parseRgbColor, parseStl, parseVox, planePolygons, polyCameraTargetToCss, polyCssDistanceToWorld, polyCssPositionToWorld, polygonCssSurfaceNormal, polygonFaces, polygonFacesCamera, repairMeshSeams, resolvePolyCameraAppliedPerspectiveStyle, ringPolygons, rotateVec3, seamFacetSplitPolygons, seamFacetSplitReport, seamOverlapDiagnostics, seamOverlapPolygons, seamOverlapReport, shadeColor, simplifyTriangleMeshPolygons, tetrahedronPolygons, torusPolygons, worldDirectionToCss, worldDirectionToPolyCss, worldDirectionalLightToCss, worldDirectionalLightToPolyCss, worldDistanceToCss, worldDistanceToPolyCss, worldPositionToCss, worldPositionToPolyCss } from '@layoutit/polycss-core';
7
7
 
8
8
  interface PolyPerspectiveCameraProps {
@@ -118,6 +118,32 @@ interface ShadowOptions {
118
118
  * disable the cap entirely.
119
119
  */
120
120
  maxExtend?: number;
121
+ /**
122
+ * Cast a low-resolution parametric silhouette per caster instead of its full
123
+ * geometry — lighter DOM + cheaper projection at the cost of an approximate
124
+ * outline. Casts onto every receiver through the normal pipeline. Default:
125
+ * `false`. (Directional + point lights; the exact path stays the default.)
126
+ */
127
+ parametric?: boolean;
128
+ /**
129
+ * Parametric-shadow detail (max silhouette points / pixel-grid resolution).
130
+ * Only used when `parametric` is true. Default: `16`. Override per mesh via
131
+ * `<PolyMesh shadowDefinition>`.
132
+ */
133
+ definition?: number;
134
+ /**
135
+ * Parametric render style: `"vector"` (smooth contour, default) or `"pixel"`
136
+ * (greedy-meshed voxel blocks; `definition` becomes the grid resolution).
137
+ */
138
+ style?: "vector" | "pixel";
139
+ /**
140
+ * Re-emit a caster's shadow while it animates (deforms) so the shadow follows
141
+ * the pose instead of freezing. Default `false`: a same-topology deform keeps
142
+ * the last shadow pose (re-emitting every frame is expensive). Best with
143
+ * `parametric` — a low-res silhouette is cheap to reproject. Topology changes
144
+ * (different polygon count) always re-emit regardless.
145
+ */
146
+ followAnimation?: boolean;
121
147
  }
122
148
 
123
149
  /** Three.js-style transform props accepted by every PolyCSS component. */
@@ -227,6 +253,9 @@ interface PolySceneProps extends TransformProps {
227
253
  rotY?: number;
228
254
  zoom?: number;
229
255
  directionalLight?: PolyDirectionalLight;
256
+ /** Point lights (world-space positions). Direction-only per-face Lambert,
257
+ * baked-mode only. */
258
+ pointLights?: PolyPointLight[];
230
259
  ambientLight?: PolyAmbientLight;
231
260
  /** Textured polygon lighting mode. Defaults to "baked". */
232
261
  textureLighting?: PolyTextureLightingMode;
@@ -275,7 +304,7 @@ interface PolySceneProps extends TransformProps {
275
304
  debugShowLabels?: boolean;
276
305
  debugShowBackfaces?: boolean;
277
306
  }
278
- declare function PolySceneInner({ polygons: polygonsProp, centerPolygons: centerPolygonsProp, perspective: _perspective, rotX: _rotX, rotY: _rotY, zoom: _zoom, directionalLight, ambientLight, textureLighting, textureQuality, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, seamBleed, strategies, autoCenter, shadow, className, style, children, position: _position, scale: _scale, rotation: _rotation, debugShowLabels: _debugShowLabels, debugShowBackfaces, }: PolySceneProps): react_jsx_runtime.JSX.Element;
307
+ declare function PolySceneInner({ polygons: polygonsProp, centerPolygons: centerPolygonsProp, perspective: _perspective, rotX: _rotX, rotY: _rotY, zoom: _zoom, directionalLight, pointLights, ambientLight, textureLighting, textureQuality, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, seamBleed, strategies, autoCenter, shadow, className, style, children, position: _position, scale: _scale, rotation: _rotation, debugShowLabels: _debugShowLabels, debugShowBackfaces, }: PolySceneProps): react_jsx_runtime.JSX.Element;
279
308
  declare const PolyScene: React$1.MemoExoticComponent<typeof PolySceneInner>;
280
309
 
281
310
  type UseMeshOptions = LoadMeshOptions;
@@ -511,6 +540,12 @@ interface PolyMeshProps extends TransformProps, InteractionProps {
511
540
  * for caster meshes — receivers handle shadow display. Defaults to `false`.
512
541
  */
513
542
  receiveShadow?: boolean;
543
+ /**
544
+ * Per-mesh parametric-shadow detail, overriding the scene's
545
+ * `shadow.definition` for this mesh's cast/self shadow. Only used when the
546
+ * scene's `shadow.parametric` is true. Unset → inherit the scene definition.
547
+ */
548
+ shadowDefinition?: number;
514
549
  /**
515
550
  * Apply mesh optimization (coplanar merge + interior cull) before
516
551
  * rendering. Defaults to `true` — matches vanilla `scene.add`'s default.
package/dist/index.js CHANGED
@@ -2638,6 +2638,7 @@ function PolySceneInner({
2638
2638
  rotY: _rotY,
2639
2639
  zoom: _zoom,
2640
2640
  directionalLight,
2641
+ pointLights,
2641
2642
  ambientLight,
2642
2643
  textureLighting = "baked",
2643
2644
  textureQuality,
@@ -2695,17 +2696,19 @@ function PolySceneInner({
2695
2696
  const sceneBbox = centerInputPolygons ? centerSceneBbox : renderSceneBbox;
2696
2697
  const directionalForAtlas = textureLighting === "dynamic" ? void 0 : directionalLight;
2697
2698
  const ambientForAtlas = textureLighting === "dynamic" ? void 0 : ambientLight;
2699
+ const pointLightsForAtlas = textureLighting === "dynamic" ? void 0 : pointLights;
2698
2700
  const polyContext = useMemo6(() => {
2699
2701
  const tileSize = 50;
2700
2702
  return {
2701
2703
  tileSize,
2702
2704
  layerElevation: tileSize,
2703
2705
  directionalLight: directionalForAtlas,
2706
+ pointLights: pointLightsForAtlas,
2704
2707
  ambientLight: ambientForAtlas,
2705
2708
  textureLighting,
2706
2709
  seamBleed
2707
2710
  };
2708
- }, [directionalForAtlas, ambientForAtlas, textureLighting, seamBleed]);
2711
+ }, [directionalForAtlas, pointLightsForAtlas, ambientForAtlas, textureLighting, seamBleed]);
2709
2712
  const autoCenterOffset = useMemo6(() => {
2710
2713
  if (!autoCenter) return [0, 0, 0];
2711
2714
  return [
@@ -2891,6 +2894,7 @@ function PolySceneInner({
2891
2894
  () => ({
2892
2895
  textureLighting,
2893
2896
  directionalLight,
2897
+ pointLights,
2894
2898
  ambientLight,
2895
2899
  strategies,
2896
2900
  seamBleed,
@@ -2907,7 +2911,7 @@ function PolySceneInner({
2907
2911
  groundCssZ,
2908
2912
  sceneEl
2909
2913
  }),
2910
- [textureLighting, directionalLight, ambientLight, strategies, seamBleed, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, shadow, registerShadowCaster, registerShadowReceiver, shadowCastersVersion, hasShadowReceiver, groundCssZ, sceneEl]
2914
+ [textureLighting, directionalLight, pointLights, ambientLight, strategies, seamBleed, textureLeafSizing, textureImageRendering, textureBackend, textureProjection, shadow, registerShadowCaster, registerShadowReceiver, shadowCastersVersion, hasShadowReceiver, groundCssZ, sceneEl]
2911
2915
  );
2912
2916
  return /* @__PURE__ */ jsx8(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ jsxs(
2913
2917
  "div",
@@ -2943,9 +2947,10 @@ import {
2943
2947
  } from "react";
2944
2948
  import {
2945
2949
  BASE_TILE as BASE_TILE3,
2950
+ buildParametricCasterOverride,
2946
2951
  buildPolyMeshTransform,
2947
2952
  buildSharedEdgeMap,
2948
- computeReceiverShadowFaces,
2953
+ computeMergedReceiverShadows,
2949
2954
  computeSceneBbox,
2950
2955
  DEFAULT_SEAM_BLEED as DEFAULT_SEAM_BLEED2,
2951
2956
  ensureCcw2D,
@@ -2958,7 +2963,8 @@ import {
2958
2963
  prepareReceiverFacePlanes,
2959
2964
  projectCssVertexToGround,
2960
2965
  resolvePolyTextureLeafGeometry as resolvePolyTextureLeafGeometry2,
2961
- worldDirectionToCss as worldDirectionToCss2
2966
+ worldDirectionToCss as worldDirectionToCss2,
2967
+ worldPositionToCss
2962
2968
  } from "@layoutit/polycss-core";
2963
2969
 
2964
2970
  // src/scene/useMesh.ts
@@ -3835,6 +3841,7 @@ var PolyMesh = forwardRef(function PolyMesh2({
3835
3841
  onFrameReady,
3836
3842
  castShadow,
3837
3843
  receiveShadow,
3844
+ shadowDefinition,
3838
3845
  merge = true,
3839
3846
  children,
3840
3847
  fallback,
@@ -4103,6 +4110,18 @@ var PolyMesh = forwardRef(function PolyMesh2({
4103
4110
  direction: inverseRotateVec3(cssLight.direction, rot)
4104
4111
  };
4105
4112
  }, [effectiveDirectional, bakedRotation]);
4113
+ const bakedPointLights = useMemo7(() => {
4114
+ const pls = sceneCtx?.pointLights;
4115
+ if (!pls || pls.length === 0) return void 0;
4116
+ const pos = position ?? [0, 0, 0];
4117
+ const rot = bakedRotation ?? [0, 0, 0];
4118
+ const hasRot = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;
4119
+ return pls.map((pl) => {
4120
+ const rel = [pl.position[0] - pos[0], pl.position[1] - pos[1], pl.position[2] - pos[2]];
4121
+ const local = hasRot ? inverseRotateVec3(rel, rot) : rel;
4122
+ return { ...pl, position: local };
4123
+ });
4124
+ }, [sceneCtx?.pointLights, position, bakedRotation]);
4106
4125
  const lightOccludedPolyIndices = void 0;
4107
4126
  const atlasPlans = useMemo7(
4108
4127
  () => {
@@ -4121,6 +4140,7 @@ var PolyMesh = forwardRef(function PolyMesh2({
4121
4140
  i,
4122
4141
  {
4123
4142
  directionalLight: bakedDirectional,
4143
+ pointLights: bakedPointLights,
4124
4144
  ambientLight: effectiveAmbient,
4125
4145
  seamBleed: seamBleedEdges?.has(i) ? effectiveSeamBleed : void 0,
4126
4146
  seamEdges: seamBleedEdges?.get(i),
@@ -4130,7 +4150,7 @@ var PolyMesh = forwardRef(function PolyMesh2({
4130
4150
  basisHints[i]
4131
4151
  ));
4132
4152
  },
4133
- [renderPolygon, directVoxelEnabled, polygons, bakedDirectional, effectiveAmbient, effectiveSeamBleed, lightOccludedPolyIndices]
4153
+ [renderPolygon, directVoxelEnabled, polygons, bakedDirectional, bakedPointLights, effectiveAmbient, effectiveSeamBleed, lightOccludedPolyIndices]
4134
4154
  );
4135
4155
  const textureAtlas = useTextureAtlas(
4136
4156
  atlasPlans,
@@ -4176,23 +4196,32 @@ var PolyMesh = forwardRef(function PolyMesh2({
4176
4196
  }
4177
4197
  return s;
4178
4198
  }, [atlasPlans, polygons]);
4199
+ const shadowCasterRegisteredRef = useRef6(false);
4200
+ const lastShadowPolyCountRef = useRef6(-1);
4179
4201
  useEffect5(() => {
4180
- if (!sceneRegisterShadowCaster) return;
4181
- if (castShadow) {
4182
- sceneRegisterShadowCaster(meshIdRef.current, {
4183
- polygons,
4184
- position: position ?? [0, 0, 0],
4185
- scale,
4186
- rotation,
4187
- renderedPolygonIndices
4188
- });
4189
- } else {
4190
- sceneRegisterShadowCaster(meshIdRef.current, null);
4191
- }
4202
+ if (!sceneRegisterShadowCaster || !castShadow) return;
4192
4203
  return () => {
4193
4204
  sceneRegisterShadowCaster(meshIdRef.current, null);
4205
+ shadowCasterRegisteredRef.current = false;
4206
+ lastShadowPolyCountRef.current = -1;
4194
4207
  };
4195
- }, [sceneRegisterShadowCaster, castShadow, polygons, position, scale, rotation, renderedPolygonIndices]);
4208
+ }, [sceneRegisterShadowCaster, castShadow]);
4209
+ useEffect5(() => {
4210
+ if (!sceneRegisterShadowCaster || !castShadow) return;
4211
+ const followAnimation = sceneCtx?.shadow?.followAnimation ?? false;
4212
+ const topologyChanged = polygons.length !== lastShadowPolyCountRef.current;
4213
+ if (shadowCasterRegisteredRef.current && !followAnimation && !topologyChanged) return;
4214
+ lastShadowPolyCountRef.current = polygons.length;
4215
+ shadowCasterRegisteredRef.current = true;
4216
+ sceneRegisterShadowCaster(meshIdRef.current, {
4217
+ polygons,
4218
+ position: position ?? [0, 0, 0],
4219
+ scale,
4220
+ rotation,
4221
+ renderedPolygonIndices,
4222
+ shadowDefinition
4223
+ });
4224
+ }, [sceneRegisterShadowCaster, castShadow, polygons, position, scale, rotation, renderedPolygonIndices, shadowDefinition, sceneCtx?.shadow]);
4196
4225
  const sceneRegisterShadowReceiver = sceneCtx?.registerShadowReceiver;
4197
4226
  useEffect5(() => {
4198
4227
  if (!sceneRegisterShadowReceiver) return;
@@ -4318,6 +4347,16 @@ var PolyMesh = forwardRef(function PolyMesh2({
4318
4347
  if (!shadowCasters || shadowCasters.size === 0) return null;
4319
4348
  const userLightDir = sceneDirectionalLight?.direction ?? [0.4, -0.7, 0.59];
4320
4349
  const lightDir = worldDirectionToCss2(userLightDir);
4350
+ const dynamicShading = effectiveTextureLighting === "dynamic";
4351
+ const scenePoints = dynamicShading ? [] : sceneCtx?.pointLights ?? [];
4352
+ const allPointLightsCss = scenePoints.map((pl) => ({
4353
+ position: worldPositionToCss(pl.position),
4354
+ color: pl.color,
4355
+ intensity: pl.intensity
4356
+ }));
4357
+ const shadowPointIndices = scenePoints.map((pl, i) => pl.castShadow ? i : -1).filter((i) => i >= 0);
4358
+ const runDirectionalShadow = !!sceneDirectionalLight?.direction && (sceneDirectionalLight.intensity ?? 1) > 0;
4359
+ const hasShadowPoints = shadowPointIndices.length > 0;
4321
4360
  const shadowLift = sceneShadow?.lift ?? 1e-3;
4322
4361
  const planes = prepareReceiverFacePlanes(
4323
4362
  polygons,
@@ -4330,18 +4369,17 @@ var PolyMesh = forwardRef(function PolyMesh2({
4330
4369
  if (planes.length === 0) return null;
4331
4370
  const casterInputs = [];
4332
4371
  for (const [casterId, data] of shadowCasters) {
4333
- const rendered = data.renderedPolygonIndices;
4334
4372
  const items = prepareCasterPolyItems(
4335
4373
  data.polygons,
4336
4374
  data.position,
4337
4375
  data.scale,
4338
- rendered ? (idx) => rendered.has(idx) : () => true,
4376
+ () => true,
4339
4377
  data.rotation ?? null
4340
4378
  );
4341
4379
  const isSelf = data.polygons === polygons;
4342
4380
  const selfMap = isSelf ? selfShadowEdgeMap : void 0;
4343
4381
  let edgeOwners;
4344
- if (!isSelf && data.polygons.length >= 40) {
4382
+ if (!isSelf && (data.polygons.length >= 40 || hasShadowPoints)) {
4345
4383
  const dposArr = data.position;
4346
4384
  const drot = data.rotation ?? null;
4347
4385
  const dsKey = JSON.stringify(data.scale ?? null);
@@ -4354,12 +4392,29 @@ var PolyMesh = forwardRef(function PolyMesh2({
4354
4392
  }
4355
4393
  edgeOwners = cachedOwners;
4356
4394
  }
4395
+ let overrideSilhouette;
4396
+ let overridePointSilhouettes;
4397
+ if (sceneShadow?.parametric) {
4398
+ const def = data.shadowDefinition ?? sceneShadow.definition ?? 16;
4399
+ const result = buildParametricCasterOverride({
4400
+ polysWorldVerts: items.map((it) => it.wv),
4401
+ lightDir,
4402
+ definition: def,
4403
+ isSelf,
4404
+ style: sceneShadow.style,
4405
+ pointLights: shadowPointIndices.map((i) => ({ position: allPointLightsCss[i].position, index: i }))
4406
+ });
4407
+ overrideSilhouette = result.overrideSilhouette;
4408
+ overridePointSilhouettes = result.overridePointSilhouettes;
4409
+ }
4357
4410
  casterInputs.push({
4358
4411
  id: casterId,
4359
4412
  items,
4360
4413
  selfShadowEdgeMap: selfMap,
4361
4414
  edgeOwners,
4362
- casterPolygonCount: data.polygons.length
4415
+ casterPolygonCount: data.polygons.length,
4416
+ overrideSilhouette,
4417
+ overridePointSilhouettes
4363
4418
  });
4364
4419
  }
4365
4420
  const cameraState = cameraCtx?.store.getState().cameraState;
@@ -4368,27 +4423,31 @@ var PolyMesh = forwardRef(function PolyMesh2({
4368
4423
  rotY: cameraState?.rotY ?? 45,
4369
4424
  meshRotation: rotation
4370
4425
  };
4371
- const specs = computeReceiverShadowFaces({
4426
+ const faces = computeMergedReceiverShadows({
4372
4427
  receiverPlanes: planes,
4373
4428
  receiverPolygons: polygons,
4374
4429
  receiverHasTexture: polygons.some((p) => p.texture !== void 0),
4375
4430
  casters: casterInputs,
4376
4431
  lightDir,
4432
+ runDirectional: runDirectionalShadow,
4433
+ pointPasses: shadowPointIndices.map((i) => ({ lightPos: allPointLightsCss[i].position, index: i })),
4434
+ allPointLights: allPointLightsCss,
4377
4435
  cameraRot,
4378
4436
  ambientLight: sceneCtx?.ambientLight,
4379
4437
  directionalLight: sceneDirectionalLight,
4380
4438
  shadow: { color: sceneShadow?.color, opacity: sceneShadow?.opacity ?? 0.25, maxExtend: sceneShadow?.maxExtend }
4381
4439
  });
4382
- return /* @__PURE__ */ jsx9(Fragment, { children: specs.map((spec) => /* @__PURE__ */ jsx9(
4440
+ if (faces.length === 0) return null;
4441
+ return /* @__PURE__ */ jsx9(Fragment, { children: faces.map((fc) => /* @__PURE__ */ jsxs2(
4383
4442
  "svg",
4384
4443
  {
4385
4444
  className: "polycss-shadow polycss-shadow-svg polycss-shadow-receiver",
4386
4445
  "data-poly-shadow-type": "receiver",
4387
- "data-poly-shadow-receiver-face": spec.faceIndex,
4388
- "data-poly-shadow-receiver-polys": JSON.stringify(spec.memberPolyIndices),
4389
- width: spec.width,
4390
- height: spec.height,
4391
- viewBox: `0 0 ${spec.width} ${spec.height}`,
4446
+ "data-poly-shadow-receiver-face": fc.faceIndex,
4447
+ "data-poly-shadow-receiver-polys": JSON.stringify(fc.memberPolyIndices),
4448
+ width: fc.width,
4449
+ height: fc.height,
4450
+ viewBox: `0 0 ${fc.width} ${fc.height}`,
4392
4451
  style: {
4393
4452
  position: "absolute",
4394
4453
  top: 0,
@@ -4398,25 +4457,27 @@ var PolyMesh = forwardRef(function PolyMesh2({
4398
4457
  transformOrigin: "0 0",
4399
4458
  pointerEvents: "none",
4400
4459
  willChange: "transform",
4401
- transform: spec.matrixCss
4460
+ opacity: fc.svgOpacity,
4461
+ transform: fc.matrixCss
4402
4462
  },
4403
- children: spec.paths.map((p, i) => /* @__PURE__ */ jsx9(
4404
- "path",
4405
- {
4406
- d: p.d,
4407
- fill: spec.fill,
4408
- stroke: spec.fill,
4409
- strokeWidth: "3",
4410
- strokeLinejoin: "round",
4411
- opacity: spec.opacity.toFixed(4),
4412
- "data-poly-shadow-caster-polys": JSON.stringify(p.casterPolygonIndices)
4413
- },
4414
- i
4415
- ))
4463
+ children: [
4464
+ fc.baseFill && fc.baseD ? /* @__PURE__ */ jsx9("path", { d: fc.baseD, fill: fc.baseFill, fillRule: "nonzero" }) : null,
4465
+ fc.layers.map((layer, i) => /* @__PURE__ */ jsx9(
4466
+ "path",
4467
+ {
4468
+ d: layer.d,
4469
+ fill: layer.fill,
4470
+ fillRule: "nonzero",
4471
+ opacity: layer.opacity !== 1 ? layer.opacity.toFixed(4) : void 0,
4472
+ style: layer.multiply ? { mixBlendMode: "multiply" } : void 0
4473
+ },
4474
+ i
4475
+ ))
4476
+ ]
4416
4477
  },
4417
- `receiver-${spec.faceIndex}`
4478
+ `receiver-${fc.faceIndex}`
4418
4479
  )) });
4419
- }, [receiveShadow, shadowCasters, shadowCastersVersion, polygons, position, scale, rotation, sceneDirectionalLight, sceneShadow, sceneCtx?.ambientLight, cameraCtx?.store, cameraTick, selfShadowEdgeMap]);
4480
+ }, [receiveShadow, shadowCasters, shadowCastersVersion, polygons, position, scale, rotation, sceneDirectionalLight, sceneCtx?.pointLights, effectiveTextureLighting, sceneShadow, sceneCtx?.ambientLight, cameraCtx?.store, cameraTick, selfShadowEdgeMap]);
4420
4481
  const portalSceneEl = sceneCtx?.sceneEl ?? null;
4421
4482
  const portaledReceiverShadowSvgs = portalSceneEl && receiverShadowSvgs ? createPortal(receiverShadowSvgs, portalSceneEl) : null;
4422
4483
  setPolygonsImplRef.current = (nextPolygons) => {
@@ -7819,7 +7880,7 @@ import {
7819
7880
  worldDirectionalLightToPolyCss,
7820
7881
  worldDistanceToCss,
7821
7882
  worldDistanceToPolyCss,
7822
- worldPositionToCss,
7883
+ worldPositionToCss as worldPositionToCss2,
7823
7884
  worldPositionToPolyCss,
7824
7885
  BASE_TILE as BASE_TILE7,
7825
7886
  DEFAULT_CAMERA_STATE as DEFAULT_CAMERA_STATE2,
@@ -7968,6 +8029,6 @@ export {
7968
8029
  worldDirectionalLightToPolyCss,
7969
8030
  worldDistanceToCss,
7970
8031
  worldDistanceToPolyCss,
7971
- worldPositionToCss,
8032
+ worldPositionToCss2 as worldPositionToCss,
7972
8033
  worldPositionToPolyCss
7973
8034
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layoutit/polycss-react",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "React components for the CSS-based polygon mesh rendering engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@layoutit/polycss-core": "^0.2.6"
44
+ "@layoutit/polycss-core": "^0.2.7"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": "^18.0.0 || ^19.0.0",