From d5adf9ccb6a2bd79a08ad6fe0372f72065bf00ef Mon Sep 17 00:00:00 2001 From: agustin-littlehat Date: Wed, 10 Jun 2026 20:52:06 -0300 Subject: [PATCH 1/2] feat: expose world CSS conversion helpers --- packages/core/src/index.ts | 6 +++ .../src/shadow/receiverFaceGroups.test.ts | 54 +++++++++++++++++++ .../core/src/shadow/receiverFaceGroups.ts | 29 +++++++++- packages/polycss/src/index.test.ts | 16 ++++++ packages/polycss/src/index.ts | 6 +++ packages/react/src/index.ts | 3 ++ packages/vue/src/index.ts | 3 ++ 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/shadow/receiverFaceGroups.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index aa73470d..0ce617d0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -173,6 +173,12 @@ export { RECEIVER_NORMAL_TOL, RECEIVER_OFFSET_TOL, RECEIVER_OUTLINE_EXPAND, + cssDistanceToWorld, + cssDistanceToWorld as polyCssDistanceToWorld, + cssPositionToWorld, + cssPositionToWorld as polyCssPositionToWorld, + worldDistanceToCss, + worldDistanceToCss as worldDistanceToPolyCss, worldCssForMesh, worldDirectionToCss, worldDirectionToCss as worldDirectionToPolyCss, diff --git a/packages/core/src/shadow/receiverFaceGroups.test.ts b/packages/core/src/shadow/receiverFaceGroups.test.ts new file mode 100644 index 00000000..7d5a02c7 --- /dev/null +++ b/packages/core/src/shadow/receiverFaceGroups.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { BASE_TILE } from "../camera/camera"; +import { + cssDistanceToWorld, + cssPositionToWorld, + worldDirectionToCss, + worldDistanceToCss, + worldPositionToCss, +} from "./receiverFaceGroups"; + +describe("world/CSS coordinate helpers", () => { + it("converts world distance to CSS pixels with the default renderer scale", () => { + expect(worldDistanceToCss(3)).toBe(3 * BASE_TILE); + expect(worldDistanceToCss(-2)).toBe(-2 * BASE_TILE); + }); + + it("converts CSS distance back to world units with the default renderer scale", () => { + expect(cssDistanceToWorld(3 * BASE_TILE)).toBe(3); + expect(cssDistanceToWorld(-2 * BASE_TILE)).toBe(-2); + }); + + it("supports an explicit world-unit pixel scale for adapters", () => { + expect(worldDistanceToCss(3, 10)).toBe(30); + expect(cssDistanceToWorld(30, 10)).toBe(3); + }); + + it("converts world position to the swapped CSS frame", () => { + expect(worldPositionToCss([3, 5, 7])).toEqual([ + 5 * BASE_TILE, + 3 * BASE_TILE, + 7 * BASE_TILE, + ]); + }); + + it("converts CSS position back to world XYZ", () => { + expect(cssPositionToWorld([5 * BASE_TILE, 3 * BASE_TILE, 7 * BASE_TILE])).toEqual([3, 5, 7]); + }); + + it("applies the explicit world-unit pixel scale to position conversions", () => { + expect(worldPositionToCss([3, 5, 7], 10)).toEqual([50, 30, 70]); + expect(cssPositionToWorld([50, 30, 70], 10)).toEqual([3, 5, 7]); + }); + + it("keeps direction conversion unitless", () => { + expect(worldDirectionToCss([3, 5, 7])).toEqual([5, 3, 7]); + }); + + it("rejects invalid world-unit pixel scales", () => { + expect(() => worldDistanceToCss(1, 0)).toThrow("positive finite"); + expect(() => cssDistanceToWorld(1, Number.NaN)).toThrow("positive finite"); + expect(() => worldPositionToCss([1, 2, 3], -1)).toThrow("positive finite"); + expect(() => cssPositionToWorld([1, 2, 3], Number.POSITIVE_INFINITY)).toThrow("positive finite"); + }); +}); diff --git a/packages/core/src/shadow/receiverFaceGroups.ts b/packages/core/src/shadow/receiverFaceGroups.ts index 325ae203..001d242a 100644 --- a/packages/core/src/shadow/receiverFaceGroups.ts +++ b/packages/core/src/shadow/receiverFaceGroups.ts @@ -29,12 +29,37 @@ export type ReceiverPlaneGroup = { memberPolyIndices: number[]; }; +function normalizeWorldUnitPx(worldUnitPx: number): number { + if (!Number.isFinite(worldUnitPx) || worldUnitPx <= 0) { + throw new Error("PolyCSS world unit size must be a positive finite number."); + } + return worldUnitPx; +} + +/** Convert a world-space scalar to CSS pixels. The default matches PolyCSS's + * current renderer scale: one world unit = BASE_TILE CSS px. */ +export function worldDistanceToCss(value: number, worldUnitPx = BASE_TILE): number { + return value * normalizeWorldUnitPx(worldUnitPx); +} + +/** Convert a CSS-pixel scalar back to PolyCSS world units. */ +export function cssDistanceToWorld(value: number, worldUnitPx = BASE_TILE): number { + return value / normalizeWorldUnitPx(worldUnitPx); +} + /** World→CSS axis swap. World is `+X right, +Y forward, +Z up`; the renderer's * internal frame swaps X↔Y and scales by BASE_TILE (one world unit = * BASE_TILE CSS px). Same conversion every renderer applies at the boundary * for mesh positions, polygon vertices, and light directions. */ -export function worldPositionToCss(p: Vec3): Vec3 { - return [p[1] * BASE_TILE, p[0] * BASE_TILE, p[2] * BASE_TILE]; +export function worldPositionToCss(p: Vec3, worldUnitPx = BASE_TILE): Vec3 { + const scale = normalizeWorldUnitPx(worldUnitPx); + return [p[1] * scale, p[0] * scale, p[2] * scale]; +} + +/** Inverse of {@link worldPositionToCss}: CSS-pixel frame → world XYZ. */ +export function cssPositionToWorld(p: Vec3, worldUnitPx = BASE_TILE): Vec3 { + const scale = normalizeWorldUnitPx(worldUnitPx); + return [p[1] / scale, p[0] / scale, p[2] / scale]; } /** World→CSS axis swap for directions (no scale; directions stay unit). The diff --git a/packages/polycss/src/index.test.ts b/packages/polycss/src/index.test.ts index af5086e2..c5e296f6 100644 --- a/packages/polycss/src/index.test.ts +++ b/packages/polycss/src/index.test.ts @@ -2,6 +2,9 @@ import { describe, expect, it } from "vitest"; import { BASE_TILE } from "@layoutit/polycss-core"; import { buildPolyMeshTransform, + polyCssDistanceToWorld, + polyCssPositionToWorld, + worldDistanceToPolyCss, worldDirectionToPolyCss, worldPositionToPolyCss, } from "./index"; @@ -19,6 +22,19 @@ describe("public transform helpers", () => { expect(worldDirectionToPolyCss([1, 2, 3])).toEqual([2, 1, 3]); }); + it("exposes scalar and inverse position conversions", () => { + expect(worldDistanceToPolyCss(3)).toBe(3 * BASE_TILE); + expect(polyCssDistanceToWorld(3 * BASE_TILE)).toBe(3); + expect(polyCssPositionToWorld([5 * BASE_TILE, 3 * BASE_TILE, 7 * BASE_TILE])).toEqual([3, 5, 7]); + }); + + it("exposes custom-scale conversions for external adapters", () => { + expect(worldPositionToPolyCss([3, 5, 7], 10)).toEqual([50, 30, 70]); + expect(worldDistanceToPolyCss(3, 10)).toBe(30); + expect(polyCssDistanceToWorld(30, 10)).toBe(3); + expect(polyCssPositionToWorld([50, 30, 70], 10)).toEqual([3, 5, 7]); + }); + it("exposes the mesh wrapper transform builder", () => { expect(buildPolyMeshTransform({ position: [1, 2, 3], diff --git a/packages/polycss/src/index.ts b/packages/polycss/src/index.ts index 48e58093..db20048e 100644 --- a/packages/polycss/src/index.ts +++ b/packages/polycss/src/index.ts @@ -20,6 +20,12 @@ export type { } from "./api/createPolyScene"; export { buildPolyMeshTransform, + cssDistanceToWorld, + cssDistanceToWorld as polyCssDistanceToWorld, + cssPositionToWorld, + cssPositionToWorld as polyCssPositionToWorld, + worldDistanceToCss, + worldDistanceToCss as worldDistanceToPolyCss, worldDirectionToPolyCss, worldPositionToPolyCss, } from "@layoutit/polycss-core"; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index c3883f55..9bd6b6ec 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -258,6 +258,9 @@ export { LoopOnce, LoopRepeat, LoopPingPong, + cssDistanceToWorld as polyCssDistanceToWorld, + cssPositionToWorld as polyCssPositionToWorld, + worldDistanceToCss as worldDistanceToPolyCss, worldDirectionToCss as worldDirectionToPolyCss, worldPositionToCss as worldPositionToPolyCss, } from "@layoutit/polycss-core"; diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 2dd573e1..877dd9e6 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -249,6 +249,9 @@ export { LoopOnce, LoopRepeat, LoopPingPong, + cssDistanceToWorld as polyCssDistanceToWorld, + cssPositionToWorld as polyCssPositionToWorld, + worldDistanceToCss as worldDistanceToPolyCss, worldDirectionToCss as worldDirectionToPolyCss, worldPositionToCss as worldPositionToPolyCss, } from "@layoutit/polycss-core"; From 1663f1080638d8e0ac53d1a34afff542aabd9aa3 Mon Sep 17 00:00:00 2001 From: agustin-littlehat Date: Wed, 10 Jun 2026 21:04:39 -0300 Subject: [PATCH 2/2] docs: list world CSS conversion helpers --- README.md | 2 +- packages/core/README.md | 2 +- packages/polycss/README.md | 2 +- packages/react/README.md | 2 +- packages/vue/README.md | 2 +- website/src/content/docs/api/headless.mdx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fb5f471c..ff15dbd0 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ export default function App() { - `autoCenter` shifts the mesh bbox center to local origin. - `meshResolution` chooses `"lossy"` (default) or `"lossless"` optimization. STL imports use the conservative lossless path in both modes. - `castShadow` emits CSS-projected shadows in dynamic lighting mode. -- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, and `worldDirectionToPolyCss` for renderer-compatible transforms. +- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, and `polyCssPositionToWorld` for renderer-compatible transforms and world/CSS conversions. ### Controls diff --git a/packages/core/README.md b/packages/core/README.md index 4b34b40f..94c00043 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -101,7 +101,7 @@ import { PolyCamera, PolyScene, PolyOrbitControls, PolyMesh } from "@layoutit/po - `autoCenter` shifts the mesh bbox center to local origin. - `meshResolution` chooses `"lossy"` (default) or `"lossless"` optimization. Lossy also applies bounded seam repair; STL imports use the conservative lossless path in both modes. - `castShadow` emits CSS-projected shadows in dynamic lighting mode. -- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, and `worldDirectionToPolyCss` for renderer-compatible transforms. +- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, and `polyCssPositionToWorld` for renderer-compatible transforms and world/CSS conversions. ### Controls diff --git a/packages/polycss/README.md b/packages/polycss/README.md index 9972cdf3..09b5721e 100644 --- a/packages/polycss/README.md +++ b/packages/polycss/README.md @@ -101,7 +101,7 @@ import { PolyCamera, PolyScene, PolyOrbitControls, PolyMesh } from "@layoutit/po - `autoCenter` shifts the mesh bbox center to local origin. - `meshResolution` chooses `"lossy"` (default) or `"lossless"` optimization. Lossy also applies bounded seam repair; STL imports use the conservative lossless path in both modes. - `castShadow` emits CSS-projected shadows in dynamic lighting mode. -- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, and `worldDirectionToPolyCss` for renderer-compatible transforms. +- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, and `polyCssPositionToWorld` for renderer-compatible transforms and world/CSS conversions. ### Controls diff --git a/packages/react/README.md b/packages/react/README.md index 4b34b40f..94c00043 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -101,7 +101,7 @@ import { PolyCamera, PolyScene, PolyOrbitControls, PolyMesh } from "@layoutit/po - `autoCenter` shifts the mesh bbox center to local origin. - `meshResolution` chooses `"lossy"` (default) or `"lossless"` optimization. Lossy also applies bounded seam repair; STL imports use the conservative lossless path in both modes. - `castShadow` emits CSS-projected shadows in dynamic lighting mode. -- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, and `worldDirectionToPolyCss` for renderer-compatible transforms. +- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, and `polyCssPositionToWorld` for renderer-compatible transforms and world/CSS conversions. ### Controls diff --git a/packages/vue/README.md b/packages/vue/README.md index 4b34b40f..94c00043 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -101,7 +101,7 @@ import { PolyCamera, PolyScene, PolyOrbitControls, PolyMesh } from "@layoutit/po - `autoCenter` shifts the mesh bbox center to local origin. - `meshResolution` chooses `"lossy"` (default) or `"lossless"` optimization. Lossy also applies bounded seam repair; STL imports use the conservative lossless path in both modes. - `castShadow` emits CSS-projected shadows in dynamic lighting mode. -- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, and `worldDirectionToPolyCss` for renderer-compatible transforms. +- Tooling can reuse `buildPolyMeshTransform`, `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, and `polyCssPositionToWorld` for renderer-compatible transforms and world/CSS conversions. ### Controls diff --git a/website/src/content/docs/api/headless.mdx b/website/src/content/docs/api/headless.mdx index db2cd87d..549e550d 100644 --- a/website/src/content/docs/api/headless.mdx +++ b/website/src/content/docs/api/headless.mdx @@ -490,7 +490,7 @@ The vanilla package also exposes renderer and atlas building blocks for diagnost - Atlas planning/rendering: `computeTextureAtlasPlanPublic`, `buildAtlasPages`, `renderPolygonsWithTextureAtlas`, `renderPolygonsWithTextureAtlasAsync`, `filterAtlasPlans`, `packTextureAtlasPlansWithScale`, `buildTextureEdgeRepairSets`. - Stable DOM animation: `renderPolygonsWithStableTriangles`, `updatePolygonsWithStableTopology`, `updateStableTriangleFrame`. -- Scene transform helpers: `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `buildPolyMeshTransform`. +- Scene transform helpers: `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, `polyCssPositionToWorld`, `buildPolyMeshTransform`. - Strategy and CSS helpers: `getSolidPaintDefaults`, `getSolidPaintDefaultsFromPlans`, `isBorderShapeSupported`, `isSolidTriangleSupported`, `isFullRectSolid`, `isSolidTrianglePlan`, `isProjectiveQuadPlan`, `cssBorderShapeForPlan`, `formatMatrix3d`, `formatCssLengthPx`, `formatSolidQuadEntryMatrix`, `formatBorderShapeEntryMatrix`. - Related types: `TextureAtlasPlan`, `PackedTextureAtlasEntry`, `PackedAtlas`, `PackedPage`, `TextureAtlasPage`, `SolidPaintDefaults`, `SolidTriangleFrame`, `PolygonBasisInfo`.