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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +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.

### Controls

Expand Down
1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +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.

### Controls

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,14 @@ export {
RECEIVER_OUTLINE_EXPAND,
worldCssForMesh,
worldDirectionToCss,
worldDirectionToCss as worldDirectionToPolyCss,
worldDirectionalLightToCss,
worldPositionToCss,
worldPositionToCss as worldPositionToPolyCss,
} from "./shadow/receiverFaceGroups";
export type { ReceiverPlaneGroup } from "./shadow/receiverFaceGroups";
export { buildPolyMeshTransform } from "./transform/meshTransform";
export type { PolyMeshTransformInput } from "./transform/meshTransform";
export {
buildSharedEdgeMap,
computeReceiverShadowFaces,
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/transform/meshTransform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { BASE_TILE } from "../camera/camera";
import { buildPolyMeshTransform } from "./meshTransform";

describe("buildPolyMeshTransform", () => {
it("returns undefined for an identity transform", () => {
expect(buildPolyMeshTransform({})).toBeUndefined();
expect(buildPolyMeshTransform({ position: [0, 0, 0], rotation: [0, 0, 0], scale: 1 })).toBeUndefined();
});

it("converts world position into the renderer CSS frame", () => {
expect(buildPolyMeshTransform({ position: [1, 2, 3] })).toBe(
`translate3d(${2 * BASE_TILE}px, ${1 * BASE_TILE}px, ${3 * BASE_TILE}px)`,
);
});

it("swaps rotation axes and flips angle sense", () => {
expect(buildPolyMeshTransform({ rotation: [10, 20, 30] })).toBe(
"rotateY(-10deg) rotateX(-20deg) rotateZ(-30deg)",
);
});

it("combines position, rotation, and scale in wrapper order", () => {
expect(buildPolyMeshTransform({ position: [1, 0, 0], rotation: [0, 0, 90], scale: [2, 3, 4] })).toBe(
`translate3d(0px, ${BASE_TILE}px, 0px) rotateZ(-90deg) scale3d(2, 3, 4)`,
);
});
});
37 changes: 37 additions & 0 deletions packages/core/src/transform/meshTransform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Vec3 } from "../types";
import { worldPositionToCss } from "../shadow/receiverFaceGroups";

export interface PolyMeshTransformInput {
position?: Vec3;
scale?: number | Vec3;
rotation?: Vec3;
}

/**
* Build the mesh wrapper transform used by every renderer for PolyCSS's
* world-frame mesh transform contract.
*/
export function buildPolyMeshTransform(
t: PolyMeshTransformInput,
): string | undefined {
const sx = typeof t.scale === "number" ? t.scale : (t.scale?.[0] ?? 1);
const sy = typeof t.scale === "number" ? t.scale : (t.scale?.[1] ?? 1);
const sz = typeof t.scale === "number" ? t.scale : (t.scale?.[2] ?? 1);
const hasScale = sx !== 1 || sy !== 1 || sz !== 1;
const hasRotation = !!t.rotation && (!!t.rotation[0] || !!t.rotation[1] || !!t.rotation[2]);
const cssPos = t.position ? worldPositionToCss(t.position) : [0, 0, 0] as Vec3;

const parts: string[] = [];
if (cssPos[0] !== 0 || cssPos[1] !== 0 || cssPos[2] !== 0) {
parts.push(`translate3d(${cssPos[0]}px, ${cssPos[1]}px, ${cssPos[2]}px)`);
}
if (hasRotation) {
if (t.rotation![0]) parts.push(`rotateY(${-t.rotation![0]}deg)`);
if (t.rotation![1]) parts.push(`rotateX(${-t.rotation![1]}deg)`);
if (t.rotation![2]) parts.push(`rotateZ(${-t.rotation![2]}deg)`);
}
if (hasScale) {
parts.push(`scale3d(${sx}, ${sy}, ${sz})`);
}
return parts.length > 0 ? parts.join(" ") : undefined;
}
1 change: 1 addition & 0 deletions packages/polycss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +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.

### Controls

Expand Down
2 changes: 2 additions & 0 deletions packages/polycss/src/api/createPolyScene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ describe("createPolyScene", () => {
scene = makeScene(host);
const sceneEl = host.querySelector(".polycss-scene");
expect(sceneEl).not.toBeNull();
expect(scene.sceneElement).toBe(sceneEl);
expect(scene.sceneElement.parentElement).toBe(scene.cameraEl);
});

it("renders the scene element as a 0x0 anchor at center (top:50%/left:50%)", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/polycss/src/api/createPolyScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,7 @@ export function createPolyScene(
host,
camera,
cameraEl,
sceneElement: sceneEl,
applyCamera,
getOptions,
meshes: listMeshes,
Expand Down
23 changes: 2 additions & 21 deletions packages/polycss/src/api/scene/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* Constants are exported so call sites can compare against them.
*/
import { BASE_TILE } from "@layoutit/polycss-core";
import { BASE_TILE, buildPolyMeshTransform } from "@layoutit/polycss-core";
import type { Polygon, Vec3 } from "@layoutit/polycss-core";
import type {
PolyPerspectiveCameraHandle,
Expand Down Expand Up @@ -81,26 +81,7 @@ export function worldDirectionalLightToCss<
export function buildMeshTransform(
t: PolyMeshTransform,
): string | undefined {
const sx = typeof t.scale === "number" ? t.scale : (t.scale?.[0] ?? 1);
const sy = typeof t.scale === "number" ? t.scale : (t.scale?.[1] ?? 1);
const sz = typeof t.scale === "number" ? t.scale : (t.scale?.[2] ?? 1);
const hasScale = sx !== 1 || sy !== 1 || sz !== 1;
const hasRotation = !!t.rotation && (!!t.rotation[0] || !!t.rotation[1] || !!t.rotation[2]);
const cssPos = t.position ? worldPositionToCss(t.position) : [0, 0, 0] as Vec3;

const parts: string[] = [];
if (cssPos[0] !== 0 || cssPos[1] !== 0 || cssPos[2] !== 0) {
parts.push(`translate3d(${cssPos[0]}px, ${cssPos[1]}px, ${cssPos[2]}px)`);
}
if (hasRotation) {
if (t.rotation![0]) parts.push(`rotateY(${-t.rotation![0]}deg)`);
if (t.rotation![1]) parts.push(`rotateX(${-t.rotation![1]}deg)`);
if (t.rotation![2]) parts.push(`rotateZ(${-t.rotation![2]}deg)`);
}
if (hasScale) {
parts.push(`scale3d(${sx}, ${sy}, ${sz})`);
}
return parts.length > 0 ? parts.join(" ") : undefined;
return buildPolyMeshTransform(t);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/polycss/src/api/scene/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ export interface PolySceneHandle {
* FPV controls toggle `.polycss-fpv-host` on this element.
*/
readonly cameraEl: HTMLElement;
/**
* The `.polycss-scene` root element inside `cameraEl`. Mesh wrappers, shadow
* roots, and helper DOM are mounted under this element.
*/
readonly sceneElement: HTMLElement;
/**
* The camera handle this scene is bound to. Controls update camera state
* via `scene.camera.update({...})` then call `scene.applyCamera()` to
Expand Down
31 changes: 31 additions & 0 deletions packages/polycss/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { BASE_TILE } from "@layoutit/polycss-core";
import {
buildPolyMeshTransform,
worldDirectionToPolyCss,
worldPositionToPolyCss,
} from "./index";

describe("public transform helpers", () => {
it("exposes the world-to-CSS position conversion used by scene meshes", () => {
expect(worldPositionToPolyCss([3, 5, 7])).toEqual([
5 * BASE_TILE,
3 * BASE_TILE,
7 * BASE_TILE,
]);
});

it("exposes the world-to-CSS direction conversion without scaling", () => {
expect(worldDirectionToPolyCss([1, 2, 3])).toEqual([2, 1, 3]);
});

it("exposes the mesh wrapper transform builder", () => {
expect(buildPolyMeshTransform({
position: [1, 2, 3],
rotation: [10, 20, 30],
scale: 2,
})).toBe(
`translate3d(${2 * BASE_TILE}px, ${1 * BASE_TILE}px, ${3 * BASE_TILE}px) rotateY(-10deg) rotateX(-20deg) rotateZ(-30deg) scale3d(2, 2, 2)`,
);
});
});
5 changes: 5 additions & 0 deletions packages/polycss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export type {
PolyMeshTransform,
PolySceneOptions,
} from "./api/createPolyScene";
export {
buildPolyMeshTransform,
worldDirectionToPolyCss,
worldPositionToPolyCss,
} from "@layoutit/polycss-core";

// ── Camera factories ──────────────────────────────────────────────
export { createPolyPerspectiveCamera, createPolyOrthographicCamera, createPolyCamera } from "./api/createPolyCamera";
Expand Down
1 change: 1 addition & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +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.

### Controls

Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type {
OptimizeMeshParseResultOptions,
OptimizeAnimatedMeshPolygonsOptions,
SimplifyTriangleMeshPolygonsOptions,
PolyMeshTransformInput,
} from "@layoutit/polycss-core";
export {
CAMERA_BACKFACE_CULL_EPS,
Expand Down Expand Up @@ -244,6 +245,7 @@ export {
torusPolygons,
planePolygons,
buildSceneContext,
buildPolyMeshTransform,
computeSceneBbox,
BASE_TILE,
DEFAULT_CAMERA_STATE,
Expand All @@ -256,4 +258,6 @@ export {
LoopOnce,
LoopRepeat,
LoopPingPong,
worldDirectionToCss as worldDirectionToPolyCss,
worldPositionToCss as worldPositionToPolyCss,
} from "@layoutit/polycss-core";
54 changes: 2 additions & 52 deletions packages/react/src/scene/PolyMesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
} from "@layoutit/polycss-core";
import {
BASE_TILE,
buildPolyMeshTransform,
buildSharedEdgeMap,
computeReceiverShadowFaces,
computeSceneBbox,
Expand Down Expand Up @@ -189,57 +190,6 @@ export interface PolyMeshProps extends TransformProps, InteractionProps {
style?: CSSProperties;
}

/**
* Build the mesh wrapper's CSS transform from a Three.js-style transform
* (post-parity convention). All transforms pivot at the wrapper's local
* origin (0,0,0) to match three.js `mesh.position`/`mesh.rotation`/`mesh.scale`
* and vanilla `buildMeshTransform`. Callers that want "rotate around
* centroid" pre-center the geometry at load time via
* `loadMesh(..., { center: true })`.
* - `position` is in WORLD UNITS (`+X right, +Y forward, +Z up`); the
* renderer applies the world→CSS axis swap (`world.x → CSS.y`,
* `world.y → CSS.x`) and ×`BASE_TILE` scale here.
* - `scale` pivots from the wrapper local origin.
* - `rotation` pivots from the wrapper local origin.
*
* Mirror of the vanilla `buildMeshTransform` in
* `packages/polycss/src/api/scene/transforms.ts`.
*/
function buildTransform(
position: Vec3 | undefined,
scale: number | Vec3 | undefined,
rotation: Vec3 | undefined,
): string | undefined {
const sx = typeof scale === "number" ? scale : (scale?.[0] ?? 1);
const sy = typeof scale === "number" ? scale : (scale?.[1] ?? 1);
const sz = typeof scale === "number" ? scale : (scale?.[2] ?? 1);
const hasScale = sx !== 1 || sy !== 1 || sz !== 1;
const hasRotation = !!rotation && (!!rotation[0] || !!rotation[1] || !!rotation[2]);
// World→CSS axis swap + ×BASE_TILE on `position`.
const cssPos: Vec3 = position
? [position[1] * BASE_TILE, position[0] * BASE_TILE, position[2] * BASE_TILE]
: [0, 0, 0];

const parts: string[] = [];
if (cssPos[0] !== 0 || cssPos[1] !== 0 || cssPos[2] !== 0) {
parts.push(`translate3d(${cssPos[0]}px, ${cssPos[1]}px, ${cssPos[2]}px)`);
}
if (hasRotation) {
// World↔CSS reflection conjugation: worldPositionToCss permutes
// [x,y,z]→[y,x,z] (det=-1), so a world rotation R(n,θ) becomes
// R(M·n, -θ) in CSS frame — axis swapped AND angle inverted.
// World X↦CSS Y, world Y↦CSS X, world Z↦CSS Z. Mirrors vanilla
// `buildMeshTransform` in packages/polycss/src/api/scene/transforms.ts.
if (rotation![0]) parts.push(`rotateY(${-rotation![0]}deg)`);
if (rotation![1]) parts.push(`rotateX(${-rotation![1]}deg)`);
if (rotation![2]) parts.push(`rotateZ(${-rotation![2]}deg)`);
}
if (hasScale) {
parts.push(`scale3d(${sx}, ${sy}, ${sz})`);
}
return parts.length > 0 ? parts.join(" ") : undefined;
}

function recenterPolygons(polygons: Polygon[]): Polygon[] {
if (polygons.length === 0) return polygons;
const bbox = computeSceneBbox(polygons);
Expand Down Expand Up @@ -363,7 +313,7 @@ export const PolyMesh = forwardRef<PolyMeshHandle, PolyMeshProps>(function PolyM
[sourcePolygons, autoCenter]
);

const transform = buildTransform(position, scale, rotation);
const transform = buildPolyMeshTransform({ position, scale, rotation });

// ── Imperative ref handle + DOM registry ──────────────────────────────
// The handle is a stable object whose getters always read the latest
Expand Down
1 change: 1 addition & 0 deletions packages/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +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.

### Controls

Expand Down
4 changes: 4 additions & 0 deletions packages/vue/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export type {
OptimizeMeshParseResultOptions,
OptimizeAnimatedMeshPolygonsOptions,
SimplifyTriangleMeshPolygonsOptions,
PolyMeshTransformInput,
} from "@layoutit/polycss-core";
export {
CAMERA_BACKFACE_CULL_EPS,
Expand Down Expand Up @@ -235,6 +236,7 @@ export {
torusPolygons,
planePolygons,
buildSceneContext,
buildPolyMeshTransform,
computeSceneBbox,
BASE_TILE,
DEFAULT_CAMERA_STATE,
Expand All @@ -247,4 +249,6 @@ export {
LoopOnce,
LoopRepeat,
LoopPingPong,
worldDirectionToCss as worldDirectionToPolyCss,
worldPositionToCss as worldPositionToPolyCss,
} from "@layoutit/polycss-core";
Loading
Loading