Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/polycss/src/api/createPolyScene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2165,6 +2165,32 @@ describe("createPolyScene", () => {
expect((d.match(/Z/g) || []).length).toBe(1);
});

it("clips low-angle ground shadow path coordinates to the capped SVG box", () => {
scene = makeScene(host, {
textureLighting: "baked",
directionalLight: { direction: [1, 0, 0.01] },
shadow: { maxExtend: 20 },
});
scene.add(makeParseResult([sideTriangle()]), { castShadow: true, merge: false });

const shadow = host.querySelector(".polycss-shadow") as SVGSVGElement;
const width = Number(shadow.getAttribute("width"));
const height = Number(shadow.getAttribute("height"));
const d = shadow.querySelector("path")?.getAttribute("d") ?? "";
const values = (d.match(/-?\d+(?:\.\d+)?/g) ?? []).map(Number);

expect(width).toBeGreaterThan(0);
expect(width).toBeLessThanOrEqual(40);
expect(height).toBeGreaterThan(0);
expect(values.length).toBeGreaterThan(0);
for (let i = 0; i < values.length; i += 2) {
expect(values[i]).toBeGreaterThanOrEqual(-0.001);
expect(values[i]).toBeLessThanOrEqual(width + 0.001);
expect(values[i + 1]).toBeGreaterThanOrEqual(-0.001);
expect(values[i + 1]).toBeLessThanOrEqual(height + 0.001);
}
});

it("baked mode projects every polygon (no Lambert cull) so thin/open meshes don't get silhouette holes", () => {
// backTriangle has its surface normal pointing AWAY from the
// default light. We deliberately do NOT cull these by Lambert
Expand Down
11 changes: 10 additions & 1 deletion packages/polycss/src/api/createPolyScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1759,15 +1759,24 @@ export function createPolyScene(
const height = by1 - by0;
if (!(width > 0) || !(height > 0)) return false;

const clipBounds: Array<[number, number]> = [
[bx0, by0],
[bx1, by0],
[bx1, by1],
[bx0, by1],
];
let d = "";
for (const verts of polyProjections) {
const ccw = ensureCcw2D(verts);
const clipped = clipPolygonToConvex2D(ensureCcw2D(verts), clipBounds);
if (clipped.length < 3) continue;
const ccw = ensureCcw2D(clipped);
d += `M${(ccw[0]![0] - bx0).toFixed(3)},${(ccw[0]![1] - by0).toFixed(3)}`;
for (let i = 1; i < ccw.length; i++) {
d += `L${(ccw[i]![0] - bx0).toFixed(3)},${(ccw[i]![1] - by0).toFixed(3)}`;
}
d += "Z";
}
if (!d) return false;
// (No receiver-footprint subtraction.) The earlier "cut every
// receiver's hull as a CW hole" approach broke fill-rule=nonzero
// wherever a receiver overlapped the caster's silhouette: a CCW
Expand Down
15 changes: 14 additions & 1 deletion website/src/components/GalleryWorkbench/GalleryWorkbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ const LIGHT_HELPER_SELECTOR = ".dn-light-helper";
const RESPONSIVE_ZOOM_BREAKPOINT = 900;
const RESPONSIVE_ZOOM_BOTTOM_RESERVE = 72;
const RESPONSIVE_ZOOM_MIN_SCALE = 0.42;
const RESPONSIVE_SHADOW_EXTEND_BASE = 1600;
const RESPONSIVE_SHADOW_EXTEND_MIN = 600;

function clamp(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min;
Expand All @@ -184,6 +186,15 @@ function responsiveZoomScaleForViewport(width: number, height: number): number {
return clamp(Math.min(widthScale, heightScale), RESPONSIVE_ZOOM_MIN_SCALE, 1);
}

function responsiveShadowMaxExtend(value: number, viewportScale: number): number {
if (viewportScale >= 0.995) return value;
const cap = Math.max(
RESPONSIVE_SHADOW_EXTEND_MIN,
Math.round(RESPONSIVE_SHADOW_EXTEND_BASE * viewportScale),
);
return Math.min(value, cap);
}

function initialResponsiveZoomScale(): number {
if (typeof window === "undefined") return 1;
return responsiveZoomScaleForViewport(window.innerWidth, window.innerHeight);
Expand Down Expand Up @@ -806,10 +817,12 @@ export default function GalleryWorkbench() {
const { handleCameraChange } = useGuiCameraSync({ setSceneOptions });
const responsiveZoomScale = useResponsiveViewportZoomScale(viewportRef);
const renderSceneOptions = useMemo<SceneOptionsState>(() => {
if (responsiveZoomScale === 1) return sceneOptions;
const shadowMaxExtend = responsiveShadowMaxExtend(sceneOptions.shadowMaxExtend, responsiveZoomScale);
if (responsiveZoomScale === 1 && shadowMaxExtend === sceneOptions.shadowMaxExtend) return sceneOptions;
return {
...sceneOptions,
zoom: sceneOptions.zoom * responsiveZoomScale,
shadowMaxExtend,
};
}, [sceneOptions, responsiveZoomScale]);
const handleRenderCameraChange = useCallback(
Expand Down
Loading