Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Public API is **mirrored** across React and Vue. Adding a hook on one side witho
| Tag | Strategy | When chosen | Paint mechanism | Atlas memory |
|---|---|---|---|---|
| `<b>` | **Quads** | Axis-aligned rectangle, or untextured convex quad when the homography passes stability guards | `background: currentColor`; canonical quads use a 1px rectangle mapped by `matrix3d` / projective `matrix3d` with tiny solid bleed to overlap antialias seams | None |
| `<i>` | **Border-shape clipped solid** | Untextured non-rect on browsers with CSS `border-shape` (Chromium + `pointer:fine` + `hover:hover`) | `border-color: currentColor` on a fixed 16px border-shape primitive, clipped by `border-shape: polygon(...)`; polygon bbox scale is folded into `matrix3d` | None |
| `<i>` | **Border-shape clipped solid** | Untextured non-rect on browsers with CSS `border-shape` (Chromium + `pointer:fine` + `hover:hover`) | `border-color: currentColor` on a fixed 16px border-shape primitive, clipped by `border-shape: polygon(...)`; polygon bbox scale and tiny solid bleed are folded into `matrix3d` | None |
| `<s>` | **Atlas slice** | Textured polygons, or untextured non-rect on browsers without `border-shape` | `background-image` slice of packed bitmap on a canonical 1px primitive; atlas position/size are normalized to the slice, scale lives in `matrix3d`, and shared textured edges get low-alpha atlas pixels repaired during atlas generation | Bounding-rect area |
| `<u>` | **Stable solid triangle** | Opt-in for triangles via `renderPolygonsWithStableTriangles` | CSS border-color triangle trick with a fixed canonical 1px border triangle; scale lives in `matrix3d` | None |
| `<u>` | **Stable solid triangle** | Opt-in for triangles via `renderPolygonsWithStableTriangles` | CSS border-color triangle trick with a fixed canonical 1px border triangle; tiny solid bleed is folded into `matrix3d` | None |
| `<q>` | **Cast shadow leaf** | Per casting polygon when `castShadow: true` and dynamic lighting mode. Applies regardless of caster strategy — `<b>`/`<i>`/`<s>`/`<u>` all produce a `<q>` shadow because only the polygon's outline matters, not its surface. | Same `border-color: currentColor` + `border-shape: polygon(...)` as `<i>`, but transform composes `var(--shadow-proj)` to project the polygon onto the ground plane along the CSS-space light direction | None |

Strategies are ordered cheapest → most expensive. The mesher's job is to maximise `<b>` / `<i>` and minimise `<s>` (see "Meshing implications" below).
Expand Down
130 changes: 126 additions & 4 deletions packages/polycss/src/render/polyDOM.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ const NON_RECT_QUAD: Polygon = {
color: "#00ffff",
};

const MODERATE_PROJECTIVE_QUAD: Polygon = {
vertices: [
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
[0, 6, 0],
],
color: "#00ffcc",
};

const UNSTABLE_PROJECTIVE_QUAD: Polygon = {
vertices: [
[0, 0, 0],
Expand Down Expand Up @@ -424,6 +434,33 @@ describe("renderPolygonsWithTextureAtlas", () => {
result.dispose();
});

it("border-shape default bleed expands the generated paint box", () => {
const doc = {
defaultView: {
CSS: {
supports: (property: string) => property === "border-shape",
},
},
createElement: (tagName: string) => document.createElement(tagName),
} as unknown as Document;

const result = renderPolygonsWithTextureAtlas([NON_RECT_QUAD], {
doc,
tileSize: 1,
strategies: { disable: ["b"] },
});
const element = result.rendered[0].element;
const matrix = extractMatrix(element);
const xScale = Math.hypot(matrix[0], matrix[1], matrix[2]);
const yScale = Math.hypot(matrix[4], matrix[5], matrix[6]);

expect(element.tagName.toLowerCase()).toBe("i");
expect(xScale).toBeGreaterThan(2 / 16);
expect(yScale).toBeGreaterThan(2 / 16);
expect(element.style.getPropertyValue("border-shape")).toContain("polygon(");
result.dispose();
});

it("uses the atlas fallback for solid non-rect polygons on non-desktop pointers when projective quads are disabled", () => {
const canvases: Array<{ width: number; height: number; getContext: () => null }> = [];
const doc = {
Expand Down Expand Up @@ -538,8 +575,10 @@ describe("renderPolygonsWithTextureAtlas", () => {
],
color: "#ffffff",
};

const result = renderPolygonsWithTextureAtlas([obliqueQuad], { tileSize: 1 });
const result = renderPolygonsWithTextureAtlas([obliqueQuad], {
tileSize: 1,
strategies: { disable: ["b"] },
});
const element = result.rendered[0].element;
const matrix = extractMatrix(element);

Expand All @@ -548,10 +587,10 @@ describe("renderPolygonsWithTextureAtlas", () => {
expect(element.style.height).toBe("");
expect(element.style.getPropertyValue("--polycss-local-w")).toBe("");
expect(element.style.getPropertyValue("--polycss-local-h")).toBe("");
expect(matrix[0]).toBeCloseTo(10 / 16, 3);
expect(matrix[0]).toBeGreaterThan(10 / 16);
expect(matrix[1]).toBeCloseTo(0, 6);
expect(matrix[4]).toBeCloseTo(0, 6);
expect(matrix[5]).toBeCloseTo(1 / 16, 2);
expect(matrix[5]).toBeGreaterThan(1 / 16);
result.dispose();
});

Expand Down Expand Up @@ -1253,6 +1292,89 @@ describe("renderPolygonsWithTextureAtlas — strategies.disable", () => {
result.dispose();
});

it("moderately projective solid quads stay on the CSS matrix b path", () => {
const canvases: Array<{ width: number; height: number; getContext: () => null }> = [];
const doc = {
defaultView: { CSS: { supports: () => false } },
createElement(tagName: string) {
if (tagName === "canvas") {
const canvas = { width: 0, height: 0, getContext: () => null };
canvases.push(canvas);
return canvas;
}
return document.createElement(tagName);
},
} as unknown as Document;

const result = renderPolygonsWithTextureAtlas(
[MODERATE_PROJECTIVE_QUAD],
{ doc },
);
const element = result.rendered[0].element;
expect(element.tagName.toLowerCase()).toBe("b");
expect(result.rendered[0].kind).toBe("solid");
expect(element.getAttribute("style") ?? "").toContain("transform:matrix3d(");
expect(canvases).toHaveLength(0);
result.dispose();
});

it("projective guard overrides can tighten the CSS matrix b path", () => {
const canvases: Array<{ width: number; height: number; getContext: () => null }> = [];
const doc = {
defaultView: {
CSS: { supports: () => false },
__polycssProjectiveQuadGuards: { maxWeightRatio: 4 },
},
createElement(tagName: string) {
if (tagName === "canvas") {
const canvas = { width: 0, height: 0, getContext: () => null };
canvases.push(canvas);
return canvas;
}
return document.createElement(tagName);
},
} as unknown as Document;

const result = renderPolygonsWithTextureAtlas(
[MODERATE_PROJECTIVE_QUAD],
{ doc },
);
const element = result.rendered[0].element;
expect(element.tagName.toLowerCase()).toBe("s");
expect(result.rendered[0].kind).toBe("atlas");
expect(canvases).toHaveLength(1);
result.dispose();
});

it("projective guard overrides can disable guard fallback for inspection", () => {
const canvases: Array<{ width: number; height: number; getContext: () => null }> = [];
const doc = {
defaultView: {
CSS: { supports: () => false },
__polycssProjectiveQuadGuards: { disableGuards: true },
},
createElement(tagName: string) {
if (tagName === "canvas") {
const canvas = { width: 0, height: 0, getContext: () => null };
canvases.push(canvas);
return canvas;
}
return document.createElement(tagName);
},
} as unknown as Document;

const result = renderPolygonsWithTextureAtlas(
[UNSTABLE_PROJECTIVE_QUAD],
{ doc },
);
const element = result.rendered[0].element;
expect(element.tagName.toLowerCase()).toBe("b");
expect(result.rendered[0].kind).toBe("solid");
expect(element.getAttribute("style") ?? "").toContain("transform:matrix3d(");
expect(canvases).toHaveLength(0);
result.dispose();
});

it("unstable projective quads fall back to border-shape by default", () => {
const canvases: Array<{ width: number; height: number; getContext: () => null }> = [];
const doc = {
Expand Down
Loading
Loading