# PolyCSS -- Full Documentation > A CSS polygon mesh engine. DOM-native 3D rendering. --- # Fonts API `@layoutit/polycss-fonts` turns font outlines and text strings into plain `Polygon[]` meshes. The pure path (`parseFont`, `textPolygons`, `composeText`) has no browser globals; browser helpers handle Google font loading and canvas-backed fill textures. ## `parseFont(data, defaultCurveSteps?)` Parses an uncompressed TrueType font (`.ttf`, `glyf` outlines) into a `ParsedFont`. ```ts import { parseFont } from "@layoutit/polycss-fonts"; const bytes = await fetch("/fonts/display.ttf").then((r) => r.arrayBuffer()); const font = parseFont(bytes); ``` Unsupported formats throw clear errors: CFF/OpenType (`.otf`) and `woff` / `woff2` wrappers are not unpacked. ```ts interface FontGlyph { contours: Vec2[][]; advanceWidth: number; } interface ParsedFont { unitsPerEm: number; ascender: number; descender: number; lineGap: number; glyph(codePoint: number, curveSteps?: number): FontGlyph; } ``` ## `textPolygons(font, text, options?)` Extrudes a single line of text into a PolyCSS mesh. ```ts import { textPolygons } from "@layoutit/polycss-fonts"; const polygons = textPolygons(font, "PolyCSS", { size: 100, depth: 24, profile: "bevel", color: "#ffd166", sideColor: "#b7791f", }); ``` ```ts interface TextPolygonsOptions { size?: number; depth?: number; curveSteps?: number; letterSpacing?: number; color?: string; sideColor?: string; profile?: "flat" | "round" | "bevel" | "custom"; profileSegments?: number; } ``` ## `composeText(font, text, options?)` Composes styled, multi-line WordArt-style text. It adds alignment, line height, underline/strike bars, envelope warps, custom profiles, face materials, and optional outlines. ```ts import { composeText, resolveFace } from "@layoutit/polycss-fonts"; const polygons = composeText(font, "Poly\nCSS", { size: 90, depth: 28, align: "center", lineHeight: 1.1, warp: { shape: "arch", amount: 0.4 }, profile: { edge: "round", raised: true }, faces: { front: resolveFace({ kind: "gradient", from: "#fef08a", to: "#f97316" }), sides: { color: "#92400e" }, back: { color: "#451a03", offset: [8, -8] }, }, outline: { color: "#111827", width: 4 }, }); ``` ```ts type WarpShape = | "none" | "arch" | "archDown" | "arc" | "wave" | "bulge" | "cone" | "slantUp" | "slantDown"; interface WarpOptions { shape: WarpShape; amount?: number; } interface Face { color?: string; texture?: string; tile?: number; } interface BackFace extends Face { offset?: [number, number]; } interface FaceStop extends Face { at: number; } type Profile = | "flat" | { edge: "bevel" | "round"; raised?: boolean; segments?: number } | { curve: CubicBezier; segments?: number }; ``` ## Fills `composeText` stays pure by accepting already-resolved face textures. Browser helpers convert higher-level fill specs into `Face` objects. ```ts import { makeFillTexture, resolveFace } from "@layoutit/polycss-fonts"; const gradientUrl = makeFillTexture({ type: "gradient", from: "#22d3ee", to: "#2563eb", angle: 90, }); const face = resolveFace({ kind: "texture", color: "#ffffff", url: "/textures/bricks.svg", tile: 24, }); ``` ```ts type FillSpec = | { type: "solid" } | { type: "gradient"; from: string; to: string; angle?: number } | { type: "rainbow"; angle?: number } | { type: "image"; src: string }; type FaceFillSpec = | { kind: "solid"; color: string } | { kind: "gradient"; color?: string; from: string; to: string; angle?: number } | { kind: "rainbow"; color?: string; angle?: number } | { kind: "texture"; color?: string; url: string; tile?: number } | { kind: "image"; color?: string; src: string }; ``` ## Google Font Helpers These browser helpers use the Fontsource API/CDN to fetch plain `.ttf` files with open CORS. ```ts import { listGoogleFonts, pickWeight, googleFontUrl, loadFont, loadGoogleFont, } from "@layoutit/polycss-fonts"; const fonts = await listGoogleFonts(); const entry = fonts.find((font) => font.family === "Bungee")!; const weight = pickWeight(entry, 700); const url = googleFontUrl(entry, weight); const font = await loadGoogleFont(entry, weight); const sameFont = await loadFont(url); ``` ```ts interface FontEntry { id: string; family: string; weights: number[]; styles: string[]; subsets: string[]; defSubset: string; category: string; type: string; } type FontStyle = "normal" | "italic"; ``` ## Utility Exports `cssCubicBezier([x1, y1, x2, y2])` returns an easing function for custom edge profiles. Related exported types are `CubicBezier`, `ExtrudeProfile`, `MaterialStop`, `ComposeTextOptions`, `Profile`, `Face`, `BackFace`, `FaceStop`, `WarpShape`, `WarpOptions`, `FillSpec`, `FaceFillSpec`, `ParsedFont`, `FontGlyph`, `TextPolygonsOptions`, `FontEntry`, and `FontStyle`. --- # Headless API The headless API (`@layoutit/polycss-core`) gives you direct access to PolyCSS parsers and utilities without React, Vue, or any framework. All exports are also available from `@layoutit/polycss` (the vanilla package) so vanilla users install just one package. ## `parseObj(text, options?)` Parses an OBJ file text string into a `ParseResult`. ```ts import { parseObj } from "@layoutit/polycss-core"; const text = await fetch("/cottage.obj").then(r => r.text()); const { polygons, warnings, dispose } = parseObj(text, { targetSize: 60, defaultColor: "#cccccc", }); // polygons: Polygon[] ready for rendering // warnings: non-fatal issues found during parse dispose(); // revoke any blob URLs (OBJ rarely creates them) ``` **Options:** See [`ObjParseOptions`](/api/types/#objparseoptions). --- ## `parseGltf(buffer, options?)` Parses a GLB or glTF file into a `ParseResult`. Accepts `ArrayBuffer` or `Uint8Array`. ```ts import { parseGltf } from "@layoutit/polycss-core"; const buf = await fetch("/model.glb").then(r => r.arrayBuffer()); const { polygons, objectUrls, dispose } = parseGltf(buf, { targetSize: 60, baseUrl: new URL("/model.glb", location.href).href, }); // polygons: Polygon[] // objectUrls: blob: URLs for embedded textures (auto-revoked by dispose()) dispose(); // always call on cleanup ``` **Options:** See [`GltfParseOptions`](/api/types/#gltfparseoptions). --- ## `parseVox(buffer, options?)` Parses a MagicaVoxel `.vox` file into greedy colored polygon quads. The result also carries `voxelSource` metadata used by the baked-mode voxel fast path across vanilla, React, and Vue when the mesh remains eligible. VOX imports stay Z-up and rotate MagicaVoxel front (`-Y`) to PolyCSS forward (`+X`). ```ts import { parseVox } from "@layoutit/polycss-core"; const buf = await fetch("/model.vox").then(r => r.arrayBuffer()); const { polygons, voxelSource } = parseVox(buf, { targetSize: 60, paletteMergeDistance: 10, }); ``` **Options:** See [`VoxParseOptions`](/api/types/#voxparseoptions). --- ## `parseStl(source, options?)` Parses ASCII or binary STL triangle meshes into a `ParseResult`. Accepts `ArrayBuffer`, `Uint8Array`, or an ASCII STL string. STL imports are triangle meshes: the format has no standard units, textures, UVs, or hierarchy. The parser supports binary Magics face colors, exposes ASCII `solid` groups as `metadata.stlSolids`, repairs consistent triangle winding, orients closed manifold components outward, and uses strong supplied-normal agreement to orient open components before emitting polygons. Winding/connectivity diagnostics are exposed as `metadata.stlTopology`. ```ts import { parseStl } from "@layoutit/polycss-core"; const buf = await fetch("/part.stl").then(r => r.arrayBuffer()); const { polygons, warnings } = parseStl(buf, { targetSize: 60, defaultColor: "#888888", }); ``` **Options:** See [`StlParseOptions`](/api/types/#stlparseoptions). --- ## `parseMtl(text)` Parses an MTL file text string into a material map. ```ts import { parseMtl } from "@layoutit/polycss-core"; const { colors, textures } = parseMtl(mtlText); // colors: Record // textures: Record ``` --- ## `loadMesh(url, options?)` High-level convenience function: fetches a URL, detects OBJ, STL, glTF/GLB, or VOX by extension (`.obj`, `.stl`, `.gltf`, `.glb`, `.vox`), parses it, and returns a `ParseResult`. Mesh optimization defaults to `meshResolution: "lossy"`; pass `"lossless"` for exact planar candidates only. In lossy mode, `loadMesh()` runs the full core import optimization path after parsing: solid texture swatch baking, near-identical baked swatch color cleanup, static triangle simplification for eligible non-animated meshes, and final DOM-cost mesh optimization. Animated glTF results keep their source polygon topology so animation sampling remains index-stable. ```ts import { loadMesh } from "@layoutit/polycss-core"; const { polygons, dispose } = await loadMesh("https://polycss.com/gallery/obj/cottage.obj", { mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl", baseUrl: new URL("https://polycss.com/gallery/obj/cottage.obj", location.href).href, gltfOptions: { targetSize: 60 }, }); // use polygons... dispose(); // always call on cleanup ``` --- ## `exportPolySceneSnapshot(target)` Serializes an already-rendered PolyCSS scene to a standalone HTML document string. The function is exported by `@layoutit/polycss`. Pass the camera wrapper, scene element, host, or any descendant of the rendered scene. It can snapshot scenes rendered by the React and Vue packages because it operates on the final DOM. ```ts import { exportPolySceneSnapshot } from "@layoutit/polycss"; const html = await exportPolySceneSnapshot(scene.host); ``` The snapshot clones the current rendered `.polycss-camera` / `.polycss-scene` DOM, injects only the PolyCSS CSS needed by that snapshot, inlines CSS `url(...)` image assets as `data:image/...;base64,...`, strips scripts and inline event handlers, and emits no PolyCSS runtime import. If an asset cannot be inlined, it throws `PolySceneSnapshotError` with `code: "ASSET_INLINE_FAILED"` and the failing `url`. --- ## `normalizePolygons(polygons)` Validates a polygon array. Drops degenerate polygons (collinear, zero-area), auto-triangulates non-coplanar N-gons via fan decomposition, and strips UV arrays that don't match `vertices.length`. Returns the validated polygons plus a list of human-readable warnings describing every change made. ```ts import { normalizePolygons } from "@layoutit/polycss-core"; import type { Polygon } from "@layoutit/polycss-core"; const raw: Polygon[] = [ { vertices: [[0,0,0],[1,0,0],[0,1,0]], color: "#f00" }, { vertices: [[0,0,0],[0,0,0],[0,0,0]] }, // degenerate: will be dropped ]; const { polygons, warnings } = normalizePolygons(raw); warnings.forEach(w => console.warn(w)); ``` --- ## `mergePolygons(polygons)` Merges coplanar adjacent polygons that share the same material (color + texture). Reduces DOM element count on large flat surfaces without changing visual output. UV-aware. ```ts import { mergePolygons } from "@layoutit/polycss-core"; const merged = mergePolygons(polygons); console.log(`${polygons.length} polygons → ${merged.length} after merge`); ``` --- ## Polygon, Color, and Geometry Helpers For custom tooling, core also exports helpers for polygon faces, texture paint bounds, CSS color parsing/formatting, and mesh-transform Euler rotation. ```ts import { polygonFaces, parseHexColor, rotateVec3 } from "@layoutit/polycss-core"; const faces = polygonFaces(polygons); const color = parseHexColor("#ffcc00"); const rotated = rotateVec3([1, 0, 0], [0, 45, 0]); ``` --- ## `optimizeMeshPolygons(polygons, options?)` Runs the shared mesh-resolution optimizer. It defaults to `meshResolution: "lossy"`. `meshResolution: "lossless"` uses exact candidates; `"lossy"` also tries bounded approximate merge candidates and chooses the lowest estimated DOM render cost. Wider lossy candidates are accepted only when they clear a minimum render-cost win and do not worsen whole-mesh seam diagnostics. ```ts import { optimizeMeshPolygons } from "@layoutit/polycss-core"; const polygons = optimizeMeshPolygons(rawPolygons, { meshResolution: "lossy", }); ``` Pass `stopAtPolygonCount` when comparing candidates and you only need a result below a known DOM-leaf budget; the default optimizer still runs all candidates. --- ## `optimizeMeshParseResult(result, options?)` Runs the same parse-result import optimization used by `loadMesh()`. Use it when you call low-level parsers manually and still want the core default post-parse path. ```ts import { bakeSolidTextureSamples, optimizeMeshParseResult, parseGltf, } from "@layoutit/polycss-core"; const parsed = parseGltf(bytes, { baseUrl }); const baked = await bakeSolidTextureSamples(parsed); const optimized = optimizeMeshParseResult(baked, { meshResolution: "lossy", source: parsed, }); ``` Pass `source` when the result has gone through solid texture baking; it lets the optimizer merge near-identical baked swatch colors only on faces that were texture-backed in the source. Static triangle simplification is enabled by default for lossy non-animated parse results and is accepted only when the final optimized polygon count is lower than the baseline optimizer result. --- ## `simplifyTriangleMeshPolygons(polygons, options?)` Runs endpoint-preserving triangle decimation for solid untextured triangle groups. Collapses land on existing vertex positions, textured polygons are skipped, material/color boundaries are kept separate, and non-manifold edge vertices are locked so one bad edge does not force the whole group to be skipped. ```ts import { simplifyTriangleMeshPolygons } from "@layoutit/polycss-core"; const candidate = simplifyTriangleMeshPolygons(rawPolygons, { ratio: 0.7, preserveVertices: true, }); ``` Compare the candidate with `optimizeMeshPolygons()` before accepting it when DOM count is the deciding constraint. For imported glTF meshes that preserve source vertex identity, `vertexKeyMode: "source"` can be used as a conservative fallback candidate after the default relaxed seam-key pass. `optimizeMeshParseResult()` handles that fallback automatically. --- ## `createIsometricCamera(initial?)` Creates an isometric-style camera state object. Used internally by the imperative `createPolyScene` API and by `` (React / Vue). ```ts import { createIsometricCamera } from "@layoutit/polycss-core"; const cam = createIsometricCamera({ rotX: 65, rotY: 45, }); // cam.state: current CameraState // cam.update(partial): merge partial state // cam.getStyle(...): returns CSS property map ``` --- ## `createPolyCamera(options?)` Creates the vanilla camera handle used by `createPolyScene`. `createPolyCamera()` is the ergonomic orthographic default and is equivalent to `createPolyOrthographicCamera()`. Use `createPolyPerspectiveCamera()` when the scene needs CSS perspective, first-person controls, or stronger depth foreshortening. ```ts import { createPolyCamera, createPolyOrthographicCamera, createPolyPerspectiveCamera, } from "@layoutit/polycss"; const camera = createPolyCamera({ rotX: 65, rotY: 45, zoom: 0.8 }); const ortho = createPolyOrthographicCamera({ target: [0, 0, 0] }); const perspective = createPolyPerspectiveCamera({ perspective: 1200, distance: 300 }); ``` All camera handles expose `state`, `update(partial)`, and `getStyle()`. The React/Vue equivalents are `` / `` and ``. --- ## `createPolyScene(host, options)` Creates a vanilla imperative scene inside `host`. It injects the PolyCSS base styles, creates a `.polycss-camera` wrapper and `.polycss-scene` root, and returns a `PolySceneHandle` for adding meshes, updating scene-level options, and tearing down the scene. ```ts import { createPolyCamera, createPolyScene, loadMesh } from "@layoutit/polycss"; const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(host, { camera, textureLighting: "dynamic", directionalLight: { direction: [0.4, -0.6, 1], intensity: 1 }, shadow: { opacity: 0.28, maxExtend: 2000 }, }); const result = await loadMesh("/model.glb"); const mesh = scene.add(result, { id: "asset", position: [0, 0, 0], castShadow: true, meshResolution: "lossy", }); mesh.setTransform({ rotation: [0, 30, 0] }); mesh.rebakeAtlas(); scene.setOptions({ textureLighting: "baked" }); scene.destroy(); ``` `scene.add(mesh, opts?)` accepts a `ParseResult` from the parsers, `loadMesh`, or a primitive shape factory. `scene.cameraEl` exposes the `.polycss-camera` wrapper, and `scene.sceneElement` exposes the `.polycss-scene` root for tooling that needs to inspect or snapshot the renderer-owned DOM. See [`PolySceneOptions`, `PolyMeshTransform`, `PolySceneHandle`, and `PolyMeshHandle`](/api/types/#vanilla-scene-handle-types) for the exact handle and option shapes. --- ## `createPolyOrbitControls(scene, options?)` Attach pointer drag, wheel zoom, and an optional autorotate loop to a scene returned by `createPolyScene`. Pure additive layer: the renderer stays free of input concerns. Modelled on Three.js `OrbitControls`. Use `createPolyMapControls(scene, options?)` instead when you want map/pan-style drag (pointer pans the camera across a flat surface rather than orbiting the scene center). Use `createPolyFirstPersonControls(scene, options?)` for pointer-lock mouselook and keyboard movement. ```ts import { createPolyCamera, createPolyScene, createPolyOrbitControls, loadMesh } from "@layoutit/polycss"; const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(host, { camera }); scene.add(await loadMesh("https://polycss.com/gallery/obj/cottage.obj", { mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl", })); const controls = createPolyOrbitControls(scene, { drag: true, // default true wheel: true, // default true invert: false, // bool or sensitivity number minZoom: 0.1, maxZoom: 10, animate: { speed: 0.3, axis: "y", pauseOnInteraction: true }, }); controls.update({ animate: false }); // mutate options live controls.pause(); // pause loop + detach listeners (reversible) controls.resume(); // re-attach after pause() controls.destroy(); // hard teardown // Three.js OrbitControls-style event subscription controls.addEventListener("change", (e) => updateUi(e.camera)); controls.addEventListener("start", () => beginInteraction()); controls.addEventListener("end", () => endInteraction()); ``` ### Options | Field | Type | Default | Description | |-------|------|---------|-------------| | `drag` | `boolean` | `true` | Pointer-drag rotation. Tracks the pointer (drag-right turns the front of the scene rightward). | | `wheel` | `boolean` | `true` | Wheel / pinch zoom. | | `invert` | `boolean \| number` | `false` | Reverse drag direction (`true`) or scale sensitivity (number; negative = invert). | | `minZoom` | `number` | `0.1` | Minimum zoom scale clamp. | | `maxZoom` | `number` | `10` | Maximum zoom scale clamp. | | `dolly` | `boolean` | `false` | When `true`, wheel drives `distance` (dolly pull-back) instead of `zoom` scale. | | `minDistance` | `number` | `0` | Minimum distance clamp when `dolly` is enabled. | | `maxDistance` | `number` | `Infinity` | Maximum distance clamp when `dolly` is enabled. | | `animate` | `false \| { speed?, axis?, pauseOnInteraction? }` | `false` | Auto-rotate. `speed` is in degrees per 60 Hz-equivalent frame; the tick is `dt`-clamped at 50 ms. | ### Returned `PolyOrbitControlsHandle` | Method | Description | |--------|-------------| | `update(partial)` | Mutate options live (toggle drag/wheel/animate, change speed, etc.). | | `resume()` | Re-attach input listeners and (re)start autorotate after `pause()`. | | `pause()` | Detach input listeners and halt the autorotate loop. Reversible by `resume()`. | | `destroy()` | Hard teardown. Drops all event listeners; functionally identical to `pause()` for input/animate state. | | `addEventListener(type, fn)` | Subscribe to `'change'` (carries `{ camera: { rotX, rotY, zoom, target, distance } }`) or `'start'` / `'end'` interaction-boundary events. Mirrors Three.js OrbitControls. | | `removeEventListener(type, fn)` | Unsubscribe by exact listener reference. | | `hasEventListener(type, fn)` | Returns `true` if the listener is currently subscribed. | `'change'` fires on every camera mutation the controls trigger: pointer drag (per `pointermove`), wheel zoom (per wheel event), and autorotate (per rAF tick). `'start'` / `'end'` fire once per gesture: pointerdown→pointerup for drags, first-wheel→idle (~150 ms) for wheel bursts. Autorotate emits `'change'` only: no `'start'` / `'end'`, since there's no user gesture. ### Required scene surface `createPolyOrbitControls` reads from and writes to its scene argument via members on `PolySceneHandle`: - `scene.host: HTMLElement`: the element handlers are attached to. - `scene.camera`: the camera handle. Controls read `scene.camera.state` for current `rotX` / `rotY` / `zoom` / `target` / `distance` and call `scene.camera.update({ rotX, rotY, zoom, ... })` to apply input changes. - `scene.applyCamera()`: re-applies the scene transform from the current camera state. Controls call this after every `scene.camera.update(...)`. Anything that satisfies that subset works, so layered helpers can compose multiple controls or build their own input on top. --- ## `createPolyFirstPersonControls(scene, options?)` Adds pointer-lock mouselook, WASD / arrow movement, Space jump, and Ctrl crouch to an imperative scene. The handle can be paused/resumed, pointer-locked programmatically from a user gesture, and teleported with `setOrigin()`. ```ts import { createPolyPerspectiveCamera, createPolyScene, createPolyFirstPersonControls, } from "@layoutit/polycss"; const camera = createPolyPerspectiveCamera({ rotX: 90, rotY: 0, perspective: 1200 }); const scene = createPolyScene(host, { camera }); const fpv = createPolyFirstPersonControls(scene, { moveSpeed: 8, eyeHeight: 1.7, }); button.addEventListener("click", () => fpv.lock()); fpv.setOrigin([10, 5, 1.7]); ``` --- ## `createSelect(scene, options?)` Adds mesh selection to an imperative scene. It tracks selected `PolyMeshHandle`s, supports optional multi-select, background clearing, and a DOM bbox fallback for clicks that do not land on a polygon leaf directly. ```ts import { createSelect } from "@layoutit/polycss"; const selection = createSelect(scene, { multiple: true, onChange(meshes) { console.log(meshes.map((mesh) => mesh.id)); }, }); selection.set([meshHandle]); selection.clear(); ``` --- ## `createTransformControls(scene, options?)` Adds a translate / rotate gizmo for a mesh handle. The gizmo mutates the attached mesh with `setTransform()` and emits object-change callbacks so application state can mirror the transform. ```ts import { createTransformControls } from "@layoutit/polycss"; const transform = createTransformControls(scene, { mode: "translate", translationSnap: 10, onObjectChange(event) { console.log(event.position, event.rotation); }, }); transform.attach(meshHandle); transform.setMode("rotate"); transform.detach(); ``` --- ## `collectPolyRenderStats(root, options?)` Reads an already-rendered PolyCSS DOM subtree and returns a one-shot diagnostic snapshot. It counts mounted polygon leaves, shadow nodes, surface leaf categories, and bucket wrappers; it does not observe changes or mutate the scene. ```ts import { collectPolyRenderStats } from "@layoutit/polycss"; const stats = collectPolyRenderStats(document.querySelector(".polycss-scene"), { polygonCount: polygons.length, }); console.log(stats.mountedPolygonLeafCount, stats.surfaceLeafCounts); ``` `scopeSelector` narrows the count to matching subtrees, which is useful when helpers, floors, or gizmos share the same scene root. The same utility is exported from `@layoutit/polycss-react` and `@layoutit/polycss-vue`. **Options:** See [`PolyRenderStatsOptions`](/api/types/#polyrenderstatsoptions). --- ## `injectPolyBaseStyles(doc?)` Injects the shared PolyCSS base stylesheet into a `Document` once. Scenes and framework components call this automatically; use it directly when rendering PolyCSS DOM yourself or when preparing a custom document before mounting. ```ts import { injectPolyBaseStyles } from "@layoutit/polycss"; injectPolyBaseStyles(document); ``` The same helper is exported from `@layoutit/polycss-react` and `@layoutit/polycss-vue`. --- ## Low-level renderer utilities The vanilla package also exposes renderer and atlas building blocks for diagnostics, custom renderers, and tests. Most applications should use `createPolyScene`, framework components, or custom elements.
Advanced renderer exports - Atlas planning/rendering: `computeTextureAtlasPlanPublic`, `buildAtlasPages`, `renderPolygonsWithTextureAtlas`, `renderPolygonsWithTextureAtlasAsync`, `filterAtlasPlans`, `packTextureAtlasPlansWithScale`, `buildTextureEdgeRepairSets`. - Stable DOM animation: `renderPolygonsWithStableTriangles`, `updatePolygonsWithStableTopology`, `updateStableTriangleFrame`. - Scene transform helpers: `worldPositionToPolyCss`, `worldDirectionToPolyCss`, `worldDirectionalLightToPolyCss`, `worldDistanceToPolyCss`, `polyCssDistanceToWorld`, `polyCssPositionToWorld`, `buildPolyMeshTransform`, `buildPolySceneTransform`. - Strategy and CSS helpers: `getSolidPaintDefaults`, `getSolidPaintDefaultsFromPlans`, `isBorderShapeSupported`, `isSolidTriangleSupported`, `isFullRectSolid`, `isSolidTrianglePlan`, `isProjectiveQuadPlan`, `cssBorderShapeForPlan`, `formatMatrix3d`, `formatCssLengthPx`, `formatSolidQuadEntryMatrix`, `formatBorderShapeEntryMatrix`. - Related types: `PolyMeshTransformInput`, `PolySceneTransformInput`, `PolyDirectionalLight`, `TextureAtlasPlan`, `PackedTextureAtlasEntry`, `PackedAtlas`, `PackedPage`, `TextureAtlasPage`, `SolidPaintDefaults`, `SolidTriangleFrame`, `PolygonBasisInfo`. `buildPolySceneTransform` accepts `PolySceneTransformInput` and returns the same scene-root transform string used by the vanilla, React, and Vue camera paths.
The transform helper names are also exported by `@layoutit/polycss-react` and `@layoutit/polycss-vue`. --- ## Primitive shape factories The vanilla package exports ParseResult-compatible factories for built-in shapes. Each one wraps the matching core polygon generator and can be passed directly to `scene.add(...)`. ```ts import { createPolyCamera, createPolyScene, createPolyBox, createPolyTorus } from "@layoutit/polycss"; const camera = createPolyCamera(); const scene = createPolyScene(host, { camera }); scene.add(createPolyBox({ size: 80, color: "#ffd166" })); scene.add(createPolyTorus({ radius: 1.2, tube: 0.35, color: "#4ecdc4" }), { position: [100, 0, 0], }); ``` Available factories: `createPolyBox`, `createPolyPlane`, `createPolyRing`, `createPolyOctahedron`, `createPolySphere`, `createPolyTetrahedron`, `createPolyIcosahedron`, `createPolyDodecahedron`, `createPolyCylinder`, `createPolyCone`, and `createPolyTorus`. --- ## Custom elements (vanilla) Register the custom elements by importing the side-effect entry point: ```html ``` See the [PolyCSS README](https://github.com/LayoutitStudio/polycss/tree/main/packages/polycss) for the full custom element attribute reference. Registered element families include: - Cameras: ``, ``, ``. - Scene and geometry: ``, ``, ``. - Controls: ``, ``, ``, ``, ``. - Helpers: ``, ``. - Shapes: ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``. The root package exports the matching element classes for manual registration or subclassing.
Custom element class exports `PolySceneElement`, `PolyMeshElement`, `PolyPolygonElement`, `PolyCameraElement`, `PolyOrthographicCameraElement`, `PolyPerspectiveCameraElement`, `PolyOrbitControlsElement`, `PolyMapControlsElement`, `PolyFirstPersonControlsElement`, `PolyTransformControlsElement`, `PolySelectElement`, `PolyAxesHelperElement`, `PolyDirectionalLightHelperElement`, `PolyBoxElement`, `PolyPlaneElement`, `PolyRingElement`, `PolyOctahedronElement`, `PolySphereElement`, `PolyTetrahedronElement`, `PolyIcosahedronElement`, `PolyDodecahedronElement`, `PolyCylinderElement`, `PolyConeElement`, and `PolyTorusElement`.
--- ## Package Exports | Import path | Contents | |---|---| | `@layoutit/polycss-react` | React components, hooks, controls, selection, animation, core re-exports, render diagnostics | | `@layoutit/polycss-vue` | Vue components, composables, controls, selection, animation, core re-exports, render diagnostics | | `@layoutit/polycss` | Vanilla imperative API + custom element classes + controls + selection + render diagnostics | | `@layoutit/polycss/elements` | Side-effect: registers the PolyCSS custom elements | | `@layoutit/polycss-core` | Pure parsers / math, zero DOM: `parseObj`, `parseGltf`, `parseVox`, `parseStl`, `loadMesh`, types | | `@layoutit/polycss-fonts` | Font parsing, Google font loading, and text-to-polygon mesh generation | | `@layoutit/polycss-morph` | Prepared-model loading, retained DOM animation, morph targets, skinning, and playback; Node preparation is exported separately from `/prepare` | --- # Three.js Parity API The `*/three` subpaths expose a Three-like authoring surface on top of PolyCSS. Use them when you are porting a Three.js scene, writing docs for coding agents, or want familiar `PerspectiveCamera`, `Object3D`, `Vector3`, `lookAt`, and radian-based mesh transforms. This is still PolyCSS: it renders DOM polygon leaves with CSS transforms. It does not run Three.js at runtime, and it does not use WebGL. The parity API matches Three's parameter surface closely enough that the same scene math frames the same object with the same projection, orientation, depth ordering, and light direction. ## Imports | Package | Import | Use | |---|---|---| | Core math | `@layoutit/polycss-core/three` | Three-like classes and coordinate transforms | | Vanilla | `@layoutit/polycss/three` | Core parity surface plus scene mounting, mesh loading, and geometry helpers | | React | `@layoutit/polycss-react/three` | React components and parity classes | | Vue | `@layoutit/polycss-vue/three` | Vue components and parity classes | ## Conventions The native PolyCSS API uses PolyCSS conventions: Z-up scene math, camera rotations in degrees, and `zoom` as CSS pixels per world unit. The `*/three` subpaths intentionally use Three-style conventions: | Value | `*/three` convention | |---|---| | Coordinates | Y-up authoring space | | Mesh rotations | Radians, XYZ Euler | | Cameras | Three-like `PerspectiveCamera(fov, aspect, near, far)` and `OrthographicCamera(left, right, top, bottom, near, far)` | | Camera targeting | `camera.position.set(...)` + `camera.lookAt(...)` | | Directional lights | Three.js source vector: `light.target.position` → `light.position` | | Point lights | `light.position` in Three/Y-up space | | Vanilla lighting default | `mountPolyThreeScene(...)` defaults to `textureLighting: "baked"` | Internally, geometry is converted into PolyCSS space with `transformPolygonsToPoly`. The axis conversion is right-handed, so polygon winding and Lambert lighting stay correct. Polygon arrays passed to `transformPolygonsToPoly` or `` are interpreted as Three/Y-up coordinates. If you already have native PolyCSS polygons with their own native transform, render them with the native API instead of converting them again. For Three parity, use baked lighting as the baseline. It computes the final Lambert face color through the same directional/ambient intensity surface used by the Three-like light adapters. Dynamic lighting is still available by passing `textureLighting: "dynamic"`, but it is a live CSS-lighting path for interactive light changes, not the strict conformance mode. ## Vanilla example `@layoutit/polycss/three` is the smallest path for plain JavaScript demos, tests, CLIs, and agent-generated snippets. The scene below is authored with Three-like camera and transform objects, then mounted into a DOM host by PolyCSS. ```ts import { AmbientLight, DirectionalLight, Object3D, PerspectiveCamera, boxPolygons, mountPolyThreeScene, transformPolygonsToPoly, } from "@layoutit/polycss/three"; const host = document.querySelector("#scene")!; const camera = new PerspectiveCamera(50, 16 / 9, 0.1, 100); camera.position.set(3, 2, 5); camera.lookAt(0, 0, 0); const object = new Object3D(); object.position.set(0, 0.5, 0); object.rotation.set(0, Math.PI / 4, 0); object.scale.set(1, 1, 1); const sun = new DirectionalLight("#ffffff", 1); sun.position.set(3, 5, 4); sun.target.position.set(0, 0, 0); const polygons = transformPolygonsToPoly( boxPolygons({ size: 1, color: "#66aaff" }), object, ); mountPolyThreeScene(host, { camera, cameraOptions: { viewportHeight: 420 }, polygons, ambientLight: new AmbientLight("#ffffff", 0.35).toPolyAmbientLight(), directionalLight: sun.toPolyDirectionalLight(), }); ``` ## React example The React parity components wrap the normal ``. Keep the scene and controls from `@layoutit/polycss-react`, and import the Three-like cameras / meshes from `@layoutit/polycss-react/three`. ```tsx import { PolyOrbitControls, PolyScene } from "@layoutit/polycss-react"; import { DirectionalLight, PolyThreeMesh, PolyThreePerspectiveCamera, } from "@layoutit/polycss-react/three"; const sun = new DirectionalLight("#ffffff", 1); sun.position.set(3, 5, 4); sun.target.position.set(0, 0, 0); export function App() { return ( ); } ``` `` accepts `polygons` or `src` like the native ``. It also supports scene mesh options such as `castShadow`, `receiveShadow`, texture options, `meshResolution`, and `merge`. ## Vue example Vue mirrors the React parity surface. ```vue ``` ## Core exports All `*/three` subpaths share these core exports: | Export | Purpose | |---|---| | `Vector3` | Minimal Three-like vector class used by cameras, objects, and lights | | `Euler` | XYZ Euler rotation in radians | | `Object3D` | `position`, `rotation`, `scale`, `up`, `lookAt`, and `localToWorld` | | `PerspectiveCamera` | Three-like perspective camera that PolyCSS can render with | | `OrthographicCamera` | Three-like orthographic camera that PolyCSS can render with | | `DirectionalLight` | Three-like positioned light with `target`, convertible to PolyCSS light options | | `PointLight` | Three-like point light, convertible to PolyCSS point-light options | | `AmbientLight` | Ambient light helper, convertible to PolyCSS light options | | `threeToPolyPoint` / `polyToThreePoint` | Convert individual points between coordinate spaces | | `threeToPolyDirection` / `polyToThreeDirection` | Convert direction vectors between coordinate spaces | | `transformPointToPoly` | Apply an `Object3D` transform to one point and convert it | | `transformPolygonsToPoly` | Apply an `Object3D` transform to a `Polygon[]` and convert it | ## When not to use it Use the native API when you are building PolyCSS-first scenes, DOM inspection demos, builder output, or examples where degree-based `rotX` / `rotY` camera controls are more direct. The parity API is for Three-shaped scene authoring and ports; it is not a replacement for native PolyCSS controls or custom elements. --- # Core Types Core parser and math types are exported from `@layoutit/polycss-react`, `@layoutit/polycss-vue`, `@layoutit/polycss`, and `@layoutit/polycss-core`. DOM render diagnostic types are exported from the renderer packages: `@layoutit/polycss-react`, `@layoutit/polycss-vue`, and `@layoutit/polycss`. ## `Polygon` The atomic renderable primitive. Each visible polygon becomes one renderer-owned DOM leaf. The exact tag is an internal strategy choice: solid CSS primitives where possible, atlas slices for textured or irregular faces. ```ts interface Polygon { /** Three or more [x, y, z] world-space points, CCW winding seen from the * outside. Winding sets the face normal via the right-hand rule, and * PolyCSS backface-culls — a reversed face is invisible. */ vertices: [number, number, number][]; /** Hex (`#rgb` / `#rrggbb`) or `rgb()` / `rgba()` only — NOT CSS named * colors. Unparseable values render white on solid and baked-atlas paths, * or are replaced with "#cccccc" on the normalizing `` * path. Falls back to "#cccccc" when neither color nor texture is set. */ color?: string; /** Image URL for UV-mapped rendering. When set with `uvs`, the renderer * applies an affine UV transform. Without `uvs`, single-tile fill. */ texture?: string; /** Imported texture wrap mode for UVs outside [0, 1]. */ textureWrap?: PolyTextureWrap; /** Imported glTF texture alpha interpretation. */ textureAlphaMode?: PolyTextureAlphaMode; /** Shared material. `material.texture` takes precedence over `texture`. */ material?: PolyMaterial; /** Source-exact image metadata (URL + source rect). It does not by itself * select a direct image leaf: the resolved presentation must end up with * `backend: "image"` and source lighting, and the default `"auto"` backend * resolves to the atlas. When it does apply, there is no atlas * rasterisation and no atlas memory. */ textureImageSource?: PolyTextureImageSource; /** Backend, projection, filtering, and lighting request for this polygon's * texture. See Texture Presentation below. */ texturePresentation?: PolyTexturePresentation; /** UV coordinates: one per vertex. Must match vertices.length. * Mismatched arrays are stripped during normalizePolygons(). */ uvs?: [number, number][]; /** Source UV triangles preserved by import/merge passes for atlas rasterization. */ textureTriangles?: TextureTriangle[]; /** Importer-internal. Records that a source material requested two-sided * rendering, so optimization passes don't collapse intentional * reverse-wound faces. It is NOT a render-time flag — setting it does not * make a polygon visible from behind. Orient your winding instead. */ doubleSided?: boolean; /** User-controlled metadata. Reflected to the DOM as data-* attributes * when rendering via . Keys must have string, number, or boolean values. */ data?: Record; } ``` --- ## `PolyMaterial` Shared texture material referenced by one or more polygons. ```ts interface PolyMaterial { /** Image source. Anything CSS background-image: url(...) can use. */ texture: string; /** Optional stable identity for renderer-side dedupe/cache paths. */ key?: string; /** Source-exact image sampling (URL + source rect) for direct image leaves * that skip atlas rasterisation. */ imageSource?: PolyTextureImageSource; /** Per-material texture presentation (backend, projection, filtering, * lighting). See Texture Presentation below. */ presentation?: PolyTexturePresentation; } ``` --- ## Texture Metadata Importer-preserved texture fields used by atlas planning and rendering. ```ts interface TextureTriangle { vertices: [Vec3, Vec3, Vec3]; uvs: [Vec2, Vec2, Vec2]; } type PolyTextureWrapMode = "repeat" | "clamp-to-edge" | "mirrored-repeat"; interface PolyTextureWrap { s: PolyTextureWrapMode; t: PolyTextureWrapMode; } type PolyTextureAlphaMode = "opaque" | "mask" | "blend"; ``` --- ## Texture Presentation How a textured polygon is painted. PolyCSS has two texture backends: the **atlas** backend rasterises each polygon's local-2D bounding rect into packed atlas pages, and the **image** backend renders a direct image leaf straight from a source URL and rect — no rasterisation and no atlas memory, but source pixels are preserved as-is. ```ts /** Leaf primitive sizing for texture leaves. Default "canonical". */ type PolyTextureLeafSizing = "canonical" | "local" | "raster"; /** Requested backend. Default "auto", which currently always resolves to the * atlas — direct image leaves must be requested explicitly with "image". */ type PolyTextureBackend = "auto" | "atlas" | "image"; /** CSS `image-rendering` for the leaf. Default "auto". */ type PolyTextureImageRendering = "auto" | "pixelated"; /** Whether the image is lit by the scene or shown as authored. */ type PolyTextureImageLighting = "scene" | "source"; /** UV mapping. "projective" enables exact quad mapping where the * compositor-stability guards allow it. Default "affine". */ type PolyTextureProjection = "affine" | "projective"; interface PolyTextureImageSource { url: string; width: number; height: number; /** Sub-rect of the source image to draw. Defaults to the whole image. */ sourceRect?: { x: number; y: number; width: number; height: number }; imageRendering?: PolyTextureImageRendering; } interface PolyTexturePresentation { imageRendering?: PolyTextureImageRendering; backend?: PolyTextureBackend; lighting?: PolyTextureImageLighting; projection?: PolyTextureProjection; } ``` Direct image leaves (`backend: "image"`) are **source-lit only** — they use `lighting: "source"` and keep the source pixels untouched. A textured polygon that needs scene lighting falls back to the atlas backend. These are also scene-level defaults: `textureBackend`, `textureProjection`, and `textureImageRendering` on `PolySceneOptions` (and the matching `` props) set the default for every polygon. From weakest to strongest, the resolved presentation is: scene defaults → `material.presentation` → (for `imageRendering` only) the selected image source's own `imageRendering` → the polygon's `texturePresentation`. The image source itself resolves as `polygon.textureImageSource` over `material.imageSource`. `textureLeafSizing` is different: it is a scene/atlas-level option with **no** per-polygon override — `PolyTexturePresentation` has no leaf-sizing field. --- ## `PolyDirectionalLight` Controls the directional light for the scene. ```ts interface PolyDirectionalLight { /** Unit direction from the surface toward the distant light source. */ direction: [number, number, number]; /** Directional light color hex (default: "#ffffff"). */ color?: string; /** Directional light intensity (default: 1). */ intensity?: number; } ``` --- ## `PolyPointLight` A positional light source. **Direction-only** — there is no distance falloff; per polygon the contribution is `color · intensity · max(0, n · L̂)` where `L̂` is the unit direction from the surface to `position`. Shading is flat per face (an approximation of three.js's `PointLight(distance: 0, decay: 0)`), and is **baked-mode only** — dynamic lighting ignores `pointLights` entirely (neither surface shading nor shadows; dynamic-mode cast shadows are directional-only). ```ts interface PolyPointLight { /** World-space position of the light. */ position: [number, number, number]; /** Point light color hex (default: "#ffffff"). */ color?: string; /** Point light intensity (default: 1). */ intensity?: number; /** When true, this light casts radial SVG shadows (default: false). */ castShadow?: boolean; } ``` --- ## `PolyAmbientLight` Ambient-only light (no directional component). ```ts interface PolyAmbientLight { /** Ambient light tint hex (default: "#ffffff"). */ color?: string; /** Ambient light intensity (default: 0.4). */ intensity?: number; } ``` --- ## `PolyTextureLightingMode` Controls whether polygon lighting is baked into generated paint output or evaluated through CSS variables at runtime. ```ts type PolyTextureLightingMode = "baked" | "dynamic"; ``` --- ## `MeshResolution` Controls mesh post-processing intent. ```ts type MeshResolution = "lossless" | "lossy"; ``` `"lossless"` preserves the authored surface while applying exact reductions. `"lossy"` allows bounded geometric approximation when it reduces rendered polygon/DOM count. --- ## `TextureQuality` and `PolySeamBleed` Renderer quality controls accepted by scene, mesh, and atlas APIs. ```ts type TextureQuality = number | "auto"; type PolySeamBleed = number | "auto"; type PolySeamBleedEdgeValue = ReadonlySet | ReadonlyMap; type PolySeamBleedEdges = | ReadonlyMap | readonly (PolySeamBleedEdgeValue | undefined)[]; ``` `TextureQuality` controls atlas bitmap budget and CSS sprite size. `PolySeamBleed` controls solid-primitive overscan for detected shared seam edges. `PolySeamBleed` semantics are identical across all three renderers: a number is the requested shared-edge overscan in CSS px (no upper clamp; each edge is still fitted to what the polygon plan can safely absorb), and `"auto"` or omitting the option resolves to the built-in `1.5` px default. The same value scales the per-strategy primitive bleeds by `clamp(value / 1.5, 0, 1)` — `0` disables every bleed. --- ## `PolyRenderStrategiesOption` Diagnostic render-strategy override accepted by scenes and atlas renderers. Disabled strategies fall through to the atlas path; `` is the universal fallback and cannot be disabled. ```ts type PolyRenderStrategy = "b" | "i" | "u"; interface PolyRenderStrategiesOption { disable?: PolyRenderStrategy[]; } ``` ## `PolyRenderStats` One-shot DOM diagnostic snapshot returned by `collectPolyRenderStats`. ```ts interface PolyRenderSurfaceLeafCounts { quad: number; /** Border-shape `` leaves. */ clippedSolid: number; atlas: number; /** `` leaves, including triangles and exact corner-shape solids. */ stableTriangle: number; } interface PolyRenderStats { /** Input polygon count when supplied, otherwise mounted polygon leaf count. */ polygonCount: number; /** Mounted surface leaves, equal to the sum of surfaceLeafCounts. */ mountedPolygonLeafCount: number; /** Mounted shadow nodes: SVG shadow surfaces plus retained compatibility shadow nodes. */ shadowLeafCount: number; /** Surface leaf categories used by the renderer. */ surfaceLeafCounts: PolyRenderSurfaceLeafCounts; /** Per-leaf readiness as flagged by the renderer — NOT proof of load or * decode. Direct-image leaves are marked ready as soon as their CSS URL is * assigned, and atlas failures are not surfaced here. */ textureReadiness: PolyTextureReadiness; /** Atlas page and bitmap accounting. */ textureStats: PolyTextureRenderStats; /** Snapshot-export accounting. */ snapshotStats: PolySnapshotRenderStats; /** Camera projection + framing snapshot. */ cameraStats: PolyCameraSnapshotStats; /** Number of PolyCSS bucket wrapper nodes under the measured scope. */ bucketCount: number; } ``` --- ## `PolyRenderStatsOptions` Options for `collectPolyRenderStats`. ```ts interface PolyRenderStatsOptions { /** Source polygon count to include in the returned snapshot. */ polygonCount?: number; /** Optional selector used to count only matching rendered subtrees. */ scopeSelector?: string; } ``` --- ## Vanilla Scene Handle Types Core imperative handles returned by `createPolyScene()` and `scene.add()`. ```ts interface PolySceneOptions { camera: PolyPerspectiveCameraHandle | PolyOrthographicCameraHandle; directionalLight?: PolyDirectionalLight; pointLights?: PolyPointLight[]; ambientLight?: PolyAmbientLight; /** Defaults to "baked". */ textureLighting?: PolyTextureLightingMode; textureQuality?: TextureQuality; /** Texture leaf primitive sizing. Defaults to "canonical". */ textureLeafSizing?: PolyTextureLeafSizing; /** Default image filtering for atlas and direct image texture leaves. */ textureImageRendering?: PolyTextureImageRendering; /** Default texture backend request. Defaults to "auto". */ textureBackend?: PolyTextureBackend; /** Default texture projection request. Defaults to "affine". */ textureProjection?: PolyTextureProjection; /** Shared-edge solid overscan in CSS px; "auto" = the 1.5px default. See `PolySeamBleed` above. */ seamBleed?: PolySeamBleed; strategies?: PolyRenderStrategiesOption; autoCenter?: boolean; shadow?: PolyShadowOptions; /** Emit `data-poly-shadow-*` attribution attributes on every shadow SVG and * path, for DevTools inspection. Vanilla `createPolyScene` only. * Default: false. */ debugShadowAttrs?: boolean; } /** Exported by all three renderer packages under the same name. The React and * Vue variants omit the vanilla-only `dragDefinition`. */ interface PolyShadowOptions { /** Default: "#000000". */ color?: string; /** 0..1. Default: 0.25. */ opacity?: number; /** World units the shadow is lifted off its receiver to avoid z-fighting. * Defaults to `POLY_DEFAULT_SHADOW_LIFT` (0.05) on both the ground-plane * fallback and the `receiveShadow` face path, in all three renderers. */ lift?: number; /** Max CSS px the shadow may extend past the mesh footprint. Default: 2000. */ maxExtend?: number; /** Cast one low-resolution coverage silhouette per caster instead of * projecting full geometry. Default: false. */ parametric?: boolean; /** Parametric silhouette detail. Default: 16. Overridden per mesh by * `PolyMeshTransform.shadowDefinition`. */ definition?: number; /** Progressive refinement: definition used while the directional light is * actively being dragged, then a debounced pass re-emits at full * `definition`. Vanilla `createPolyScene` only. Unset → no progressive pass. */ dragDefinition?: number; /** "vector" (default) traces a smooth concave contour; "pixel" * greedy-meshes the coverage into blocky rectangles. */ style?: "vector" | "pixel"; /** Re-emit shadows while a mesh animates instead of freezing at the last * pose. Default false. All three renderers throttle the re-emit to the same * 80 ms window (~12fps, leading + trailing edge) — pair with a low * parametric `definition`. */ followAnimation?: boolean; } interface PolyMeshTransform { id?: string; position?: Vec3; scale?: number | Vec3; rotation?: Vec3; /** Run the mesh optimizer (coincident-face dedupe, interior-face cull, and * coplanar/lossy merging per `meshResolution`) before rendering. Defaults to * true. `false` skips all of it — for animated meshes whose triangle * topology must stay stable, or to render the polygon array exactly as * given. */ merge?: boolean; meshResolution?: MeshResolution; stableDom?: boolean; excludeFromAutoCenter?: boolean; castShadow?: boolean; receiveShadow?: boolean; /** Per-mesh parametric-shadow detail, overriding the scene's * `shadow.definition`. Only used when `shadow.parametric` is true. */ shadowDefinition?: number; } interface PolySceneHandle { add(mesh: ParseResult, opts?: PolyMeshTransform): PolyMeshHandle; setOptions(partial: Partial>): void; getOptions(): Readonly>; applyCamera(): void; meshes(): readonly PolyMeshHandle[]; /** Resolves once the renderer has flagged every mesh's textures ready. */ whenTexturesReady(): Promise; findMeshByElement(element: Element | null): PolyMeshHandle | null; destroy(): void; readonly host: HTMLElement; readonly cameraEl: HTMLElement; readonly sceneElement: HTMLElement; readonly camera: PolyPerspectiveCameraHandle | PolyOrthographicCameraHandle; } interface PolyMeshHandle { readonly element: HTMLElement; readonly id?: string; readonly transform: PolyMeshTransform; polygons: Polygon[]; setTransform(transform: Partial): void; setPolygons(polygons: Polygon[], options?: { merge?: boolean; stableDom?: boolean; recomputeAutoCenter?: boolean; }): void; updatePolygon(target: Polygon | number, partial: Partial): void; rebakeAtlas(): void; getPosition(): Vec3 | undefined; getRotation(): Vec3 | undefined; getScale(): number | Vec3 | undefined; getPolygons(): Polygon[]; /** Resolves once the renderer has flagged this mesh's texture leaves ready. * Not a guarantee that every image loaded or decoded — direct-image leaves * resolve on URL assignment, and atlas failures are not surfaced. */ whenTexturesReady(): Promise; remove(): void; dispose(): void; } ``` --- ## Framework Type Exports React and Vue export their public prop, event, context, and handle types next to the runtime components/composables. Vue mirrors the React names where applicable, with Vue-specific context/composable names such as `PolyCameraContextKey`, `PolySelectionContextKey`, `PolyContext`, and `UsePolyAnimationResultVue`. Most imperative control **options and handle** types belong to the vanilla package and are not re-exported by React or Vue — see the vanilla-only list below. The first-person pair (`PolyFirstPersonControlsOptions`, `PolyFirstPersonControlsHandle`) is the exception: both framework packages do re-export it. Runtime selection helpers are exported from the framework packages: `findPolyMeshHandle`, `pointInMeshElement`, and `findMeshUnderPoint`.
Framework type inventory - Component props: `PolyCameraProps`, `PolyOrthographicCameraProps`, `PolyPerspectiveCameraProps`, `PolySceneProps`, `PolyMeshProps`, `PolyGroundProps`, `PolyProps`, `PolyBoxProps`, `PolyPlaneProps`, `PolyRingProps`, `PolyOctahedronProps`, `PolySphereProps`, `PolyTetrahedronProps`, `PolyIcosahedronProps`, `PolyDodecahedronProps`, `PolyCylinderProps`, `PolyConeProps`, `PolyTorusProps`, `PolyAxesHelperProps`, `PolyDirectionalLightHelperProps`. - Control and selection props: `PolyOrbitControlsProps`, `PolyMapControlsProps`, `PolyTransformControlsProps`, `PolySelectProps`, `PolyControlsAnimateOptions`, `PolyOrbitControlsCamera`, `PolyMapControlsCamera`. `PolyFirstPersonControlsOptions`, `PolyFirstPersonControlsHandle`. **React only:** `PolyFirstPersonControlsProps`, `PolyControlsCamera`. - **Vanilla only** — exported from `@layoutit/polycss`, NOT from the framework packages: `PolyControlsBaseOptions`, `PolyControlsHandle`, `PolyControlsChangeEvent`, `PolyControlsInteractionEvent`, `PolyControlsEvent`, `PolyControlsListener`, `PolyOrbitControlsOptions`, `PolyOrbitControlsHandle`, `PolyMapControlsOptions`, `PolyMapControlsHandle`, `PolyTransformControlsOptions`, `PolyTransformControlsHandle`, `PolySelectOptions`, `PolySelectionHandle`, `PolyShapeResult`, `PolySceneSnapshotErrorCode`. - Hooks, events, and contexts: `UseCameraOptions`, `UseCameraResult`, `UseSceneContextResult`, `UseMeshOptions`, `UseMeshResult`, `UsePolyAnimationResult`, `PolyPointerEvent`, `PolyMouseEvent`, `PolyWheelEvent`, `PolyEventHandler`, `InteractionProps`, `PolyShadowOptions`, `PolySelectionApi`, `PolyCameraContext`, `PolyCameraContextValue`. - Utility types: `PolyTransformControlsObject`, `PolyTransformControlsObjectChangeEvent`. **React only:** `TransformProps`, `DOMPassthroughProps`.
--- ## `Vec2` A 2D point or UV coordinate. ```ts type Vec2 = [number, number]; ``` --- ## `Vec3` A 3D point or direction. ```ts type Vec3 = [number, number, number]; ``` --- ## `ParseResult` Unified return shape from all mesh parsers (`parseObj`, `parseGltf`, `parseVox`, `parseStl`, `loadMesh`). ```ts interface ParseResult { /** Parsed and validated polygon array, ready for rendering. */ polygons: Polygon[]; /** Optional raw voxel source for .vox fast paths; polygon fallback remains authoritative. */ voxelSource?: PolyVoxelSource; /** Blob URLs minted during parse (e.g. embedded GLB textures). * Revoked when dispose() is called. */ objectUrls: string[]; /** Revoke all objectUrls. Idempotent. Safe to call on unmount. */ dispose: () => void; /** Non-fatal warnings raised during parse (dropped polygons, UV mismatches, etc.). */ warnings: string[]; /** Optional animation sampler for glTF/GLB files with usable animation clips. */ animation?: ParseAnimationController; /** Optional format-specific metadata. */ metadata?: { triangleCount?: number; meshes?: string[]; materials?: string[]; animations?: ParseAnimationClip[]; sourceBytes?: number; voxelCount?: number; stlHeader?: string; stlColor?: ParseStlColor; stlSolids?: ParseStlSolid[]; stlTopology?: ParseStlTopology; }; } ``` --- ## `ParseStlTopology` STL winding/connectivity diagnostics exposed on `ParseResult.metadata.stlTopology`. The optimizer uses unreliable topology signals to avoid interior-culling shortcuts that can collapse malformed CAD meshes. ```ts interface ParseStlTopology { componentCount: number; repairedTriangleCount: number; outwardComponentCount: number; suppliedNormalComponentCount: number; inconsistentSharedEdgeCount: number; nonManifoldSharedEdgeCount: number; } ``` --- ## `ParseStlColor` and `ParseStlSolid` STL-specific metadata for binary Magics colors and ASCII `solid` groups. `stlSolids` ranges use emitted polygon indices after malformed and degenerate facets have been filtered. ```ts interface ParseStlColor { format: "magics"; defaultColor: string; alpha: number; coloredTriangleCount: number; defaultColorTriangleCount: number; } interface ParseStlSolid { name: string; start: number; count: number; } ``` --- ## `PolyVoxelSource` Raw `.vox` metadata preserved by `parseVox` for renderer fast paths. The polygon list remains the fallback and public geometry. ```ts interface PolyVoxelCell { x: number; y: number; z: number; color: string; } interface PolyVoxelSource { kind: "magica-vox"; cells: PolyVoxelCell[]; rows: number; cols: number; depth: number; scale: number; sourceBytes: number; } ``` --- ## `ParseAnimationController` glTF / GLB parses can expose a lightweight sampler for animation clips. Framework hooks and the core animation mixer consume this controller. ```ts interface ParseAnimationClip { index: number; name: string; duration: number; channelCount: number; } interface ParseAnimationController { clips: ParseAnimationClip[]; sample: (clip: number | string, timeSeconds: number) => Polygon[]; } ``` --- ## `PolyAnimationMixer` Core animation API used by `usePolyAnimation` and vanilla animation loops. ```ts interface PolyAnimationTarget { setPolygons(polygons: Polygon[]): void; } type PolyAnimationClip = ParseAnimationClip; interface PolyAnimationAction { play(): PolyAnimationAction; stop(): PolyAnimationAction; reset(): PolyAnimationAction; fadeIn(durationSeconds: number): PolyAnimationAction; fadeOut(durationSeconds: number): PolyAnimationAction; crossFadeTo(target: PolyAnimationAction, durationSeconds: number): PolyAnimationAction; crossFadeFrom(from: PolyAnimationAction, durationSeconds: number): PolyAnimationAction; setLoop(mode: LoopMode, repetitions: number): PolyAnimationAction; setEffectiveTimeScale(scale: number): PolyAnimationAction; setEffectiveWeight(weight: number): PolyAnimationAction; clampWhenFinished: boolean; timeScale: number; weight: number; time: number; enabled: boolean; paused: boolean; readonly isRunning: boolean; } interface PolyAnimationMixer { clipAction(clip: number | string): PolyAnimationAction; existingAction(clip: number | string): PolyAnimationAction | null; update(deltaSeconds: number): void; stopAllAction(): void; uncacheClip(clip: number | string): void; uncacheRoot(): void; } ``` `LoopOnce`, `LoopRepeat`, and `LoopPingPong` are exported constants matching three.js loop modes. --- ## `LoadMeshOptions` Options for the high-level `loadMesh` dispatcher. Format-specific parser settings are nested under the matching key. ```ts interface LoadMeshOptions { /** Base URL for resolving relative .gltf textures/buffers. */ baseUrl?: string; /** Companion .mtl URL for OBJ models. Ignored for STL/glTF/GLB/VOX. */ mtlUrl?: string; /** Forwarded to parseObj; merged with materials from mtlUrl when present. */ objOptions?: ObjParseOptions; /** Forwarded to parseGltf. */ gltfOptions?: GltfParseOptions; /** Forwarded to parseVox. */ voxOptions?: VoxParseOptions; /** Forwarded to parseStl. */ stlOptions?: StlParseOptions; /** Convert uniform texture-backed faces into solid-color polygons before optimization. */ solidTextureSamples?: boolean | SolidTextureSampleOptions; /** Shared mesh-resolution optimizer. Defaults to "lossy". */ meshResolution?: "lossless" | "lossy"; } ``` `loadMesh()` applies `optimizeMeshParseResult()` internally. In default lossy mode this includes baked swatch color cleanup, static triangle simplification for eligible non-animated meshes, and final DOM-cost mesh optimization. --- ## `OptimizeMeshParseResultOptions` Options for `optimizeMeshParseResult()`, the core post-parse optimization path used by `loadMesh()`. ```ts interface OptimizeMeshParseResultOptions { meshResolution?: "lossless" | "lossy"; source?: ParseResult; bakedTextureColorMergeDistance?: number; simplifyTriangleMeshes?: boolean; simplifyTriangleMeshOptions?: SimplifyTriangleMeshPolygonsOptions; simplifyEarlyStopDropRatio?: number; } ``` --- ## `SolidTextureSampleOptions` Optional `loadMesh` optimization that converts texture-backed faces whose sampled UV region is effectively a uniform color into solid-color polygons before culling and merging. ```ts interface SolidTextureSampleOptions { /** Set false to keep every textured polygon texture-backed. */ enabled?: boolean; /** Per-channel tolerance for declaring sampled texels uniform. Default: 2. */ colorTolerance?: number; /** Skip decoding very large textures for this optimization. Default: 16 MP. */ maxTexturePixels?: number; } ``` --- ## `NormalizeResult` Return type of `normalizePolygons`. ```ts interface NormalizeResult { polygons: Polygon[]; warnings: string[]; } ``` --- ## `ObjParseOptions` Options for `parseObj`. ```ts interface ObjParseOptions { /** Scale the model so its longest axis is this many world units. */ targetSize?: number; /** Fallback color for un-colored faces (default: "#888888"). */ defaultColor?: string; /** Override per-material colors (material name → CSS color). */ materialColors?: Record; /** Override per-material textures (material name → image URL). */ materialTextures?: Record; /** Only include these named OBJ objects. */ includeObjects?: string[]; /** Exclude these named OBJ objects. */ excludeObjects?: string[]; /** Quantize vertex colors to this palette. */ palette?: string[]; } ``` --- ## `VoxParseOptions` Options for `parseVox`. ```ts interface VoxParseOptions { /** Scale near this longest-axis size; snapped to integer voxel CSS cells. Default: 60. */ targetSize?: number; /** Lossy RGB distance for folding nearby opaque palette colors before greedy meshing. Default: disabled. */ paletteMergeDistance?: number; /** Lossy RGB distance for recoloring small local color islands/streaks before greedy meshing. Default: disabled. */ colorRegionMergeDistance?: number; } ``` --- ## `StlParseOptions` Options for `parseStl`. STL is a triangle mesh format; it has no standard units, textures, UVs, or hierarchy. Binary Magics colors are parsed when the STL header declares `COLOR=rgba`. ```ts interface StlParseOptions { /** Scale the model so its longest axis is this many world units. */ targetSize?: number; /** Shift all vertices by this amount after scaling. */ gridShift?: number; /** Solid color assigned to every STL triangle. */ defaultColor?: string; /** Source up axis. Default "z"; "y" applies the OBJ/glTF cyclic remap. */ upAxis?: "z" | "y"; } ``` --- ## `GltfParseOptions` Options for `parseGltf`. ```ts interface GltfParseOptions { /** Scale the model so its longest axis is this many world units. */ targetSize?: number; /** Fallback color for un-colored faces (default: "#888888"). */ defaultColor?: string; /** Override per-material colors. */ materialColors?: Record; /** Override per-material textures (material name -> image URL). */ materialTextures?: Record; /** Treat the model's up axis as Y or Z. Most GLB files use Y. */ upAxis?: "y" | "z"; /** Base URL for resolving external texture references in .gltf files. */ baseUrl?: string; /** Custom buffer resolver for external .bin files. */ resolveBuffer?: (uri: string) => Promise | Uint8Array; } ``` --- ## `MtlParseResult` Return type of `parseMtl`. ```ts interface MtlParseResult { /** Material name → CSS color. */ colors: Record; /** Material name → texture image path. */ textures: Record; } ``` --- ## `AutoRotateOption` Configures automatic camera rotation. ```ts type AutoRotateOption = | boolean // true = default Y-axis at speed 0.3, pauseOnInteraction | number // speed in degrees/frame on Y axis | { axis?: "x" | "y"; speed?: number; pauseOnInteraction?: boolean; }; ``` ## `CameraState` Internal camera state (used by `createIsometricCamera`). ```ts interface CameraState { target: [number, number, number]; rotX: number; rotY: number; zoom: number; distance: number; // dolly pull-back in pixels (default 0); adds translateZ(-distance)px } ``` ## Camera Handles Vanilla camera handles returned by `createPolyCamera`, `createPolyOrthographicCamera`, and `createPolyPerspectiveCamera`. ```ts interface PolyCameraOptions { zoom?: number; target?: Vec3; rotX?: number; rotY?: number; distance?: number; } interface PolyPerspectiveCameraOptions extends PolyCameraOptions { perspective?: number; } interface PolyOrthographicCameraOptions extends PolyCameraOptions {} interface CameraStyleInput { rows?: number; cols?: number; } interface CameraHandle { readonly state: CameraState; update(next: Partial): void; getStyle(input?: CameraStyleInput): { transform: string; width: string; height: string; }; } interface PolyPerspectiveCameraHandle extends CameraHandle { readonly type: "perspective"; readonly perspectiveStyle: string; } interface PolyOrthographicCameraHandle extends CameraHandle { readonly type: "orthographic"; readonly perspectiveStyle: "none"; } ``` --- # PolyCamera import { Tabs, TabItem } from '@astrojs/starlight/components'; PolyCSS provides two camera components: `` (alias ``) for parallel projection, and `` for scenes with depth foreshortening. The camera element is normally the **outer** node — `` / `PolyScene` is nested inside it. The CSS rendering model requires this nesting: CSS `perspective` only applies to descendants, so the scene's `transform: matrix3d(...)` must be a child of the element carrying the projection. React and Vue enforce it — `PolyScene` throws outside a camera component, and `createPolyScene()` takes a required camera handle. The `` custom element is the one exception: with no ancestor camera element it builds an **implicit** camera wrapper from its own `perspective`, `rot-x`, `rot-y`, `zoom`, `distance`, and `target` attributes, which satisfies the same nesting requirement internally. See [PolyScene](/components/poly-scene). `` is orthographic by default. Use `` when depth foreshortening is needed (e.g. first-person or game-like scenes). For pointer drag, wheel zoom, and autorotate, drop a `` child inside the scene: see **[PolyOrbitControls](/components/poly-controls)**. ## Perspective vs Orthographic | Use case | Component | |----------|-----------| | Isometric / voxel / diagrammatic scenes | `` (alias ``) — **default** | | 3D scenes with depth foreshortening | `` | PolyCSS defaults to orthographic rather than perspective — PolyCSS's strengths (integer-pixel atlas, no per-frame JS, DOM stacking) are most visible in ortho scenes. This deliberately diverges from three.js's default. ## `PolyCamera` / `PolyOrthographicCamera` `PolyCamera` is an alias for `PolyOrthographicCamera`. Import either: they are identical. Uses `perspective: none` (parallel projection). No `perspective` prop. ### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `zoom` | `number` | `0.65` | On-screen CSS pixels per world unit — at `zoom: 50` one world unit renders 50 px across. Higher zooms in, lower zooms out; orbit controls clamp to `0.1`–`10` by default (`minZoom` / `maxZoom`). | | `rotX` | `number` | `65` | Rotation around the X axis in degrees. | | `rotY` | `number` | `45` | Rotation around the Y axis in degrees (0–360). | | `distance` | `number` | `0` | Dolly pull-back in pixels. When > 0, adds `translateZ(-distance)px` to the scene transform. Driven by `dolly` mode in `PolyOrbitControls`. | | `target` | `Vec3` | `[0,0,0]` | Point in scene space the camera orbits around. | ## `PolyPerspectiveCamera` Adds CSS `perspective` for depth foreshortening. Use for game-like or first-person scenes. ### Props All props from `PolyCamera` above, plus: | Prop | Type | Default | Description | |------|------|---------|-------------| | `perspective` | `number` | `32000` | CSS perspective depth in pixels. Higher values feel flatter (more isometric); lower values exaggerate depth. | ## Usage ### Basic Camera Set an initial angle. `PolyCamera` (orthographic) is the default. ```html ``` ```tsx import { PolyCamera, PolyScene, PolyCone } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ```vue ``` ### Orthographic Camera (default) `` / `PolyCamera` is orthographic. `PolyOrthographicCamera` is the explicit alias — use either. ```html ``` ```tsx import { PolyCamera, PolyScene, PolyIcosahedron } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ```vue ``` ### Interactive Camera with Auto-rotation (recommended) Drop a `` child for drag, wheel, and `dt`-clamped autorotate. Works the same across vanilla, React, and Vue. ```html ``` ```tsx // React ``` Full options reference: **[PolyOrbitControls](/components/poly-controls)**. ## `distance`: dolly pull-back `distance` adds a `translateZ(-distance)px` to the scene transform. Unlike `zoom`, which scales the entire scene, `distance` moves the viewpoint back along the view axis: equivalent to increasing the spherical radius in three.js `OrbitControls`. Enable `dolly` on `PolyOrbitControls` to let the wheel drive `distance` instead of `zoom`. ## `usePolyCamera` hook (React) `usePolyCamera` is used internally by ``. Use `` as the wrapper whenever a React or Vue scene needs camera state, pointer controls, wheel controls, or autorotate. ## Related - [PolyScene](/components/poly-scene): The scene component that renders meshes. - [PolyOrbitControls](/components/poly-controls): Pointer drag, wheel zoom, dolly, and autorotate. - [Core Concepts: Camera](/core-concepts#camera): Conceptual explanation of camera props. - [Quickstart](/quickstart): Install + first scene walkthrough. --- # Controls import { Tabs, TabItem } from '@astrojs/starlight/components'; PolyCSS ships additive controls that follow the three.js split: - **`PolyOrbitControls`**: orbit-style drag (pointer drag rotates around the scene center) + wheel zoom + autorotate. This is the default pick for most scenes. - **`PolyMapControls`**: map/pan-style drag (pointer drag pans the camera across the surface). Use for top-down or flat layouts. - **`PolyFirstPersonControls`**: pointer-lock mouselook plus WASD / arrow movement, jump, and crouch. - **`PolyTransformControls`**: translate / rotate gizmo for a selected mesh handle. Camera controls mutate the wrapping `` / `PolyCamera` state. Transform controls mutate the attached mesh handle via `setTransform`. They are available as custom elements, imperative APIs, and React / Vue components. ## Orbit / Map Props (React / Vue prop names use camelCase; the `` / `` custom elements accept the kebab-case form, e.g. `animate.speed` → `animate-speed`.) | Prop | Type | Default | Description | |------|------|---------|-------------| | `drag` | `boolean` | `true` | Pointer-drag rotation. Drag-right turns the front of the scene rightward, drag-down tilts the top toward the user: the visible object tracks the pointer. | | `wheel` | `boolean` | `true` | Wheel / pinch zoom. Trackpad pinch is delivered as `wheel` with `ctrlKey=true`, so this covers desktop scroll + Mac pinch in one path. | | `invert` | `boolean \| number` | `false` | Drag-direction inversion. `true` reverses; a number scales sensitivity in the default direction (negative = invert). | | `minZoom` | `number` | `0.1` | Minimum zoom scale clamp. | | `maxZoom` | `number` | `10` | Maximum zoom scale clamp. | | `dolly` | `boolean` | `false` | When `true`, the wheel drives `distance` (dolly pull-back) instead of `zoom` scale. See **Dolly mode** below. | | `minDistance` | `number` | `0` | Minimum distance clamp when `dolly` is enabled. | | `maxDistance` | `number` | `Infinity` | Maximum distance clamp when `dolly` is enabled. | | `animate` | `false \| { speed?, axis?, pauseOnInteraction? }` | `false` | Auto-rotate. Pass `false` (or omit) to disable. See **Animate options** below. | ### Animate options | Field | Type | Default | Description | |-------|------|---------|-------------| | `speed` | `number` | `0.3` | Degrees per 60 Hz-equivalent frame. The tick is `dt`-clamped (max 50 ms per frame), so `0.3` ≈ 18 deg/sec on every refresh rate. | | `axis` | `"x" \| "y"` | `"y"` | Rotation axis. `"y"` orbits the camera horizontally; `"x"` tilts vertically. | | `pauseOnInteraction` | `boolean` | `true` | Halt the animate loop while a pointer drag is in progress; resume on pointer-up. | ## Usage ### Basic: drag + wheel + slow autorotate ```html ``` The presence of any `animate-*` attribute (`animate-speed`, `animate-axis`, `animate-pause-on-interaction`) implies `animate` is enabled. Removing them all turns animate off. ```tsx import { PolyCamera, PolyScene, PolyOrbitControls, PolyTorus } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ```vue ``` ### Imperative: vanilla JS When you mount a scene through the `createPolyScene` imperative API, pair it with `createPolyOrbitControls(scene, options)`. The controls handle returns a small lifecycle interface for live updates. ```ts import { createPolyCamera, createPolyScene, createPolyOrbitControls, createPolyTorus } from "@layoutit/polycss"; const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(host, { camera }); scene.add(createPolyTorus({ color: "#4ecdc4" })); const controls = createPolyOrbitControls(scene, { drag: true, wheel: true, animate: { speed: 0.3, axis: "y", pauseOnInteraction: true }, }); // Later: toggle features live without re-creating: controls.update({ animate: false }); // stop auto-rotate controls.update({ drag: false }); // also disable pointer drag // Pause everything (detaches listeners + cancels rAF): reversible: controls.pause(); controls.resume(); // Hard teardown: controls.destroy(); // Three.js OrbitControls-style event subscription: controls.addEventListener("change", (e) => console.log(e.camera)); controls.addEventListener("start", () => console.log("interaction begin")); controls.addEventListener("end", () => console.log("interaction end")); ``` ### Map / pan mode Use `` (or `createPolyMapControls`) when you want drag to pan the camera across a flat surface rather than orbit around the scene center. ```html ``` ```tsx import { PolyCamera, PolyScene, PolyMapControls, PolyMesh } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ### Read-only / tour mode Disable input but keep autorotate running: ```html ``` ```tsx ``` ```vue ``` ### Inverted / sensitive drag ```html ``` ```tsx {/* 2× sensitivity */} {/* reverse */} ``` ## Dolly mode By default, the wheel adjusts `zoom`: a scale transform on the entire scene. Enable `dolly` to have the wheel adjust `distance` (a `translateZ` pull-back) instead. This mirrors three.js `OrbitControls` where the wheel changes the spherical radius around the target rather than scaling the projection. When to use each: - **Scale-zoom (default):** Good for 2D-map-style or isometric scenes where you want the scene to grow/shrink in place. - **Dolly (`dolly={true}`):** Good for perspective scenes where depth foreshortening should stay consistent: the camera moves back rather than the scene shrinking. ```tsx // React: dolly mode with clamped range ``` ```ts // Vanilla: createPolyOrbitControls const controls = createPolyOrbitControls(scene, { dolly: true, minDistance: 100, maxDistance: 3000, }); ``` ## How it works `PolyOrbitControls` and `PolyMapControls` are purely additive: they attach their own pointer/wheel listeners and run their own `requestAnimationFrame` loop when `animate` is on. In vanilla, state changes flow through `scene.setOptions(...)`; in React/Vue, they mutate the shared camera context and apply the transform directly. The animate tick is `dt`-clamped at 50 ms per frame and normalized to 60 Hz. That makes `speed: 0.3` produce the same ~18 deg/sec on every monitor refresh rate (60, 120, 144 Hz) and survives a tab regaining focus without a giant catch-up jump. ## First-person Controls Use `PolyFirstPersonControls` for walkable scenes. Click the scene to acquire pointer lock; Escape releases it. Movement is keyboard-driven (`WASD` / arrows, Space jump, Ctrl crouch) and mouselook updates camera pitch/yaw. | Prop | Type | Default | Description | |------|------|---------|-------------| | `enabled` | `boolean` | `true` | Master switch. | | `lookEnabled` | `boolean` | `true` | Pointer-lock mouselook. | | `moveEnabled` | `boolean` | `true` | WASD / arrow-key planar movement. | | `jumpEnabled` | `boolean` | `true` | Space-bar jump arc. | | `crouchEnabled` | `boolean` | `true` | Ctrl crouch. | | `lookSensitivity` | `number` | `0.15` | Degrees per pointer pixel. | | `invertY` | `boolean` | `false` | Invert vertical look. | | `moveSpeed` | `number` | `5` | World units per second. | | `jumpVelocity` | `number` | `7` | Initial jump velocity. | | `gravity` | `number` | `18` | Jump gravity. | | `eyeHeight` | `number` | `1.7` | Standing eye height above `groundZ`. | | `crouchHeight` | `number` | `1` | Crouched eye height. | | `groundZ` | `number` | `0` | Walk plane height. | | `minPitch` / `maxPitch` | `number` | `5` / `175` | Pitch clamp in degrees. | ```tsx import { PolyPerspectiveCamera, PolyScene, PolyFirstPersonControls, PolyMesh, } from "@layoutit/polycss-react"; ``` Imperative handles expose `lock()`, `unlock()`, `isLocked()`, `getOrigin()`, `setOrigin()`, `pause()`, `resume()`, `destroy()`, and `update(partial)`. ## Transform Controls `PolyTransformControls` attaches to a `PolyMeshHandle` and renders a PolyCSS gizmo. Translate mode provides axis arrows and plane handles; rotate mode provides axis rings. Dragging updates the attached mesh directly and emits transform-change callbacks. ```tsx import { useState } from "react"; import { PolyCamera, PolyScene, PolyMesh, PolySelect, PolyTransformControls, type PolyMeshHandle, } from "@layoutit/polycss-react"; function Editor() { const [selected, setSelected] = useState(null); return ( setSelected(meshes[0] ?? null)}> ); } ``` Key props: `object`, `mode`, `size`, `showX`, `showY`, `showZ`, `translationSnap`, `rotationSnap`, `enabled`, `onChange`, `onObjectChange`, `onMouseDown`, `onMouseUp`, and `onDraggingChanged`. ## Related - [PolyScene](/components/poly-scene): The render root for meshes and polygons. - [PolyCamera](/components/poly-camera): Required camera context wrapper for React / Vue scenes and controls. - [Headless API: `createPolyOrbitControls`](/api/headless#createpolyorbitcontrols): Full imperative API reference. --- # PolyScene import { Tabs, TabItem } from '@astrojs/starlight/components'; The scene is the root of every PolyCSS render tree. It applies scene-level lighting and atlas options, then renders its children (typically meshes or individual polygons) in 3D space. `PolyScene` (React/Vue) must be nested inside a camera component (`PolyCamera`, `PolyPerspectiveCamera`, or `PolyOrthographicCamera`) — it throws otherwise — and `createPolyScene()` takes a required camera handle in its options. The `` custom element prefers an ancestor camera element, but can stand alone: without one it builds an implicit camera from its own `perspective`, `rot-x`, `rot-y`, `zoom`, `distance`, and `target` attributes. It's available as a custom element (``), via the imperative `createPolyScene(host, opts)` API, and as React / Vue components (``). ## Scene props / attributes (React / Vue prop names use camelCase; the `` custom element accepts the kebab-case form, e.g. `textureQuality` → `texture-quality`.) The React/Vue components and `createPolyScene()` support the full table except rows marked otherwise (`polygons`, `children`, and `centerPolygons` are framework-only; `shadow.dragDefinition` — and the imperative-only `debugShadowAttrs` option, not listed here — are vanilla-only). The `` custom element supports `directional-*`, `ambient-*`, `texture-lighting`, `texture-quality`, `texture-leaf-sizing`, `texture-image-rendering`, `texture-backend`, `texture-projection`, `auto-center`, and — when no ancestor camera element is present — the implicit camera attributes `perspective`, `rot-x`, `rot-y`, `zoom`, `distance`, and `target`. Only `perspective`, `rot-x`, `rot-y`, and `zoom` are *observed*: mutating `distance` or `target` alone does not update the implicit camera — their current values are read at connect time and re-applied only when a `rot-x`, `rot-y`, or `zoom` mutation next fires. (`perspective` likewise only selects the camera type at connect.) Use the imperative API for options such as `shadow`, `seamBleed`, and `strategies` in vanilla. | Prop | Type | Default | Description | |------|------|---------|-------------| | `directionalLight` | `PolyDirectionalLight` | None | Directional light source. | | `pointLights` | `PolyPointLight[]` | None | Positional lights (direction-only, no falloff). Baked mode only; set `castShadow: true` per light for radial shadows. | | `ambientLight` | `PolyAmbientLight` | None | Ambient fill light. | | `textureLighting` | `"baked" \| "dynamic"` | `"baked"` | Whether texture lighting is rasterized into atlases or computed with CSS variables. | | `textureQuality` | `number \| "auto"` | `"auto"` | Atlas bitmap budget and compositor sprite size. Auto caps large runtime bitmaps and uses a larger desktop sprite to avoid Safari/Firefox flattening artifacts; lower numeric values reduce texture memory and detail. | | `textureLeafSizing` | `"canonical" \| "local" \| "raster"` | `"canonical"` | Texture leaf CSS primitive sizing. | | `textureImageRendering` | `"auto" \| "pixelated"` | `"auto"` | Default image filtering for atlas and direct-image texture leaves. | | `textureBackend` | `"auto" \| "atlas" \| "image"` | `"auto"` | Default texture backend request per polygon. `"auto"` currently always resolves to the atlas — direct image leaves must be requested explicitly with `"image"`. | | `textureProjection` | `"affine" \| "projective"` | `"affine"` | Default texture projection request for textured quads. | | `seamBleed` | `number \| "auto"` | `1.5` | Overscan on detected shared solid seam edges, identical across all three renderers. A number is the requested CSS-pixel amount with no upper clamp — every request is still fitted per edge to what the polygon plan can safely absorb. `"auto"` (or omitting the option) resolves to the built-in `1.5` px default. The same option scales the per-strategy primitive bleeds by `clamp(value / 1.5, 0, 1)`: `0` disables every bleed, values below `1.5` shrink primitive bleeds proportionally, and larger values keep them at full strength while the seam overscan uses the raw pixel amount. | | `strategies` | `{ disable?: ("b" \| "i" \| "u")[] }` | None | Diagnostic override for render strategy selection. Disabled solid strategies fall through to `` atlas slices; `` cannot be disabled. | | `autoCenter` | `boolean` | `false` | Rotate around the content bbox center instead of world origin. Polygon data is not mutated. | | `centerPolygons` | `Polygon[]` | None | (Framework only.) Bbox source for `autoCenter` when renderable polygons live inside child meshes. | | `shadow` | `{ color?, opacity?, lift?, maxExtend?, parametric?, definition?, style?, dragDefinition?, followAnimation? }` | `{ color:"#000000", opacity:0.25, lift:0.05, maxExtend:2000 }` | Appearance + SVG extent cap for cast shadows. `parametric` swaps to a cheap low-res silhouette (`definition` = detail, `style: "vector" \| "pixel"`); `dragDefinition` (vanilla) and `followAnimation` cover light-drag and animated meshes. See [Parametric shadows](/guides/lighting/#parametric-shadows). | | `polygons` | `Polygon[]` | None | (Framework only.) Flat array of polygon objects rendered as direct children. Composes with JSX/slot children. | | `children` | None | None | Meshes, polygons, controls, helpers, selection wrappers, and transform controls. | **Camera state and input** are normally set on the wrapping camera element (`` / `PolyCamera`): `rot-x`, `rot-y`, `zoom`, `distance`. Without an ancestor camera element, `` falls back to an implicit camera driven by its own `perspective`, `rot-x`, `rot-y`, `zoom`, `distance`, and `target` attributes (`distance` and `target` are read at connect and on the next `rot-x` / `rot-y` / `zoom` change — they are not live-observed on their own). Add a child `` / `` to enable drag, wheel, or autorotate: see [PolyOrbitControls](/components/poly-controls). ## Mesh props / attributes React/Vue `` supports the full table. The `` custom element supports `src`, `mtl`, `mesh-resolution`, `position`, `scale`, `rotation`, `auto-center`, `cast-shadow`, `receive-shadow`, plus the OBJ parse attributes `target-size`, `default-color`, `palette`, `include-objects`, and `exclude-objects` (these five affect `.obj` sources only). `position`, `scale`, `rotation`, `cast-shadow`, and `receive-shadow` update live; changing `src`, `mtl`, `mesh-resolution`, or any of the OBJ parse attributes tears the mesh down and reloads it; `auto-center` is read at load only, so changing it after mount does nothing. Use `scene.add(result, opts)` for vanilla options the element doesn't expose, such as `merge`, `stableDom`, and `shadowDefinition`. | Prop | Type | Description | |------|------|-------------| | `id` | `string` | Stable mesh identifier. Reflected as `data-poly-mesh-id` and exposed on mesh handles for selection / transform tools. | | `src` | `string` | URL to `.obj`, `.stl`, `.glb`, `.gltf`, or `.vox`. | | `polygons` | `Polygon[]` | Pre-parsed polygons (alternative to `src`). Framework only. | | `position` | `Vec3` | `[x, y, z]` offset in scene space. | | `scale` | `number \| Vec3` | Uniform or per-axis scale. | | `rotation` | `Vec3` | Euler rotation in degrees `[x, y, z]`. | | `textureLighting` | `"baked" \| "dynamic"` | Per-mesh lighting mode override. React / Vue only; vanilla meshes inherit the scene value. | | `textureQuality` | `number \| "auto"` | Atlas bitmap budget and compositor sprite size. React / Vue only; vanilla meshes inherit the scene's `texture-quality`. | | `textureLeafSizing` | `"canonical" \| "local" \| "raster"` | Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value. | | `textureImageRendering` | `"auto" \| "pixelated"` | Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value. | | `textureBackend` | `"auto" \| "atlas" \| "image"` | Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value. | | `textureProjection` | `"affine" \| "projective"` | Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value. | | `seamBleed` | `number \| "auto"` | Per-mesh solid seam overscan. React / Vue only; vanilla meshes inherit the scene setting. | | `atomicAtlas` | `boolean` | Hold the previous atlas frame until the next frame is decoded, then swap atomically. React / Vue only. | | `onFrameReady` | `() => void` | Fires when an atomic atlas frame swaps to a ready one. React / Vue only. | | `autoCenter` | `boolean` | Shift the loaded mesh so its bounding-box center sits at the local origin before applying `position`. Useful when assets aren't centered in their file coordinates. | | `mtl` | `string` | Companion `.mtl` URL for OBJ models. | | `parseOptions` | `UseMeshOptions` | Parser options forwarded to `loadMesh`; `meshResolution` defaults to `"lossy"`. | | `meshResolution` | `"lossless" \| "lossy"` | Top-level optimizer intent. Wins over `parseOptions.meshResolution`; defaults to `"lossy"`. | | `castShadow` | `boolean` | Emit SVG cast shadows in both lighting modes; projections update when light, ground, or mesh geometry changes. | | `receiveShadow` | `boolean` | Casters project per-coplanar-face SVG shadows onto this mesh's visible surfaces (Three.js `mesh.receiveShadow` semantics). Defaults to `false`. In React/Vue, any receiver in the scene disables the casters' ground-shadow fallback; vanilla has no such fallback, so a receiver is required for any shadow to appear. | | `shadowDefinition` | `number` | Per-mesh parametric-shadow detail, overriding the scene's `shadow.definition` (only when `shadow.parametric`). | | `merge` | `boolean` | Run the polygon optimizer (dedupe, interior cull, coplanar/lossy merge). Defaults to `true`. Set `false` to render **the polygon array entering the renderer** exactly as given. It cannot restore source-file geometry: with `src`, `loadMesh` has already optimized the parse result before `merge` is consulted. | | `fallback` | `ReactNode` | Rendered while `src` is loading. React prop / Vue `#fallback` slot. | | `errorFallback` | `(error: Error) => ReactNode` | Rendered if parse fails. React prop / Vue `#error` slot. | | `children` | `(polygon, index) => ReactNode` | Per-polygon render prop / scoped slot. (React / Vue only.) | ## Ground Props `PolyGround` is a React/Vue convenience component for a flat shadow-receiving plane. | Prop | Type | Default | Description | |------|------|---------|-------------| | `size` | `number` | `6` | Side length in world units. | | `z` | `number` | `0` | World-space floor height. | | `center` | `[number, number]` | `[0,0]` | Ground center in world X/Y. | | `color` | `string` | `"#7d848e"` | Ground fill color. | | `className` / `class` | `string` | None | Additional CSS class. | ## PolyDirectionalLight ```ts interface PolyDirectionalLight { direction: [number, number, number]; // Surface-to-light source direction color?: string; // Light color (default: "#ffffff") intensity?: number; // Directional intensity (default: 1) } interface PolyAmbientLight { color?: string; // Ambient tint (default: "#ffffff") intensity?: number; // Ambient intensity (default: 0.4) } ``` ## Scene Helpers Helpers render as ordinary scene children and are available in vanilla custom elements plus React/Vue components. | Helper | Props | Description | |--------|-------|-------------| | `` / `PolyAxesHelper` | `size`, `thickness`, `negative`, `xColor`, `yColor`, `zColor` | Draws red/green/blue world axes from the origin. | | `` / `PolyDirectionalLightHelper` | React/Vue: `light`, `target`, `distance`, `size`, `color`. Vanilla: `direction`, `target`, `distance`, `size`, `color`. | Draws a small marker along a directional light vector. | ## Usage ### Basic Scene A scene with a dodecahedron at default camera angle. ```html ``` ```tsx import { PolyCamera, PolyScene, PolyDodecahedron } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ```vue ``` ### Scene with Camera and Lighting ```tsx import { PolyPerspectiveCamera, PolyScene, PolyTorus, PolyBox } from "@layoutit/polycss-react"; ``` ### Cast Shadows Shadows are SVG-projected surfaces that reproject when the light or scene geometry changes. Directional-light shadows work in both lighting modes. Point-light shadows are **baked mode only** — like point-light shading, they're omitted in dynamic mode (a colored point shadow over a floor those lights never lit would look broken), so dynamic-mode shadows are directional-only. Where multiple lights overlap on a face, the shadows composite to the correct both-blocked color (each light shows the others' color where it alone is blocked). ```tsx import { PolyCamera, PolyScene, PolyGround, PolyMesh } from "@layoutit/polycss-react"; ``` ### Strategy Diagnostics Use `strategies.disable` when you need to compare paths or isolate browser rendering issues. ```tsx ``` ### Multiple Meshes ```tsx ``` ### Flat Polygon Array Pass a `polygons` array directly to render static geometry without a file loader. ```tsx const polygons: Polygon[] = [ { vertices: [[0,0,0],[1,0,0],[0,1,0]], color: "#f00" }, { vertices: [[2,0,0],[3,0,0],[2,1,0]], color: "#00f" }, ]; ``` ## Related - [PolyCamera](/components/poly-camera): Camera state and projection. - [Loading Meshes](/guides/textures): OBJ, STL, glTF, GLB, VOX, MTL loading. - [Per-polygon Interaction](/guides/shapes): Using `Poly` for interactive per-polygon control. - [Performance](/guides/performance): Merge modes and DOM tuning. --- # Core Concepts import PolyDemo from '../../components/PolyDemo.astro'; This page covers the mental model behind PolyCSS. Each section describes a building block and how it composes with the others. ## A single block Where voxcss used to render a single voxel cube, PolyCSS renders any regular polyhedron the same way: each face becomes one DOM element. The demo starts on an icosahedron; flip the shape selector to see how the same renderer handles every Platonic solid, including a dodecahedron with 12 native pentagons. ## Three building blocks PolyCSS exposes three composable concepts. Each one ships as a custom element (vanilla) and as a React / Vue component: - **Scene** (`` / `PolyScene`): the render tree root. Normally nested inside a camera element (the custom element can also stand alone — see [Camera](#camera)). Sets up lighting and fills its parent element. - **Mesh** (`` / `PolyMesh`): loads a mesh from a URL (OBJ / STL / glTF / GLB / VOX). Emits one internal leaf per visible polygon. Convenience wrapper around the parser + renderer. - **Polygon** (`` / `Poly`): one polygon. The atomic primitive. Renders as one internal DOM leaf with `transform: matrix3d(...)`. Accepts standard DOM event handlers, classes, and styles: this is what makes PolyCSS "DOM-native 3D" rather than "3D inside a black-box canvas". A loaded mesh does **not** expand into `` elements — the renderer mounts one internal leaf per visible polygon inside a `.polycss-mesh` wrapper, and the leaf tag is a private strategy choice (see [Render Strategies](#render-strategies)). `` / `` exists for polygons you author yourself. To style or handle a loaded mesh per-polygon, use its render prop / scoped slot, or target the mesh wrapper and its leaves by class and `data-*` attributes. ## Camera The camera element (`` / `PolyCamera`) is normally the **outer** node, with `` / `PolyScene` nested inside it, and camera attributes (`rot-x`, `rot-y`, `zoom`, `distance`) belong on the camera element rather than the scene. `PolyCamera` is orthographic by default; use `PolyPerspectiveCamera` for depth foreshortening. **One exception:** the `` custom element can stand alone. With no ancestor camera element it builds an implicit camera from its own `perspective`, `rot-x`, `rot-y`, `zoom`, `distance`, and `target` attributes — see [PolyScene](/components/poly-scene). React/Vue have no such fallback: `PolyScene` throws outside a camera component. ```html ``` ```tsx // React ``` See **[PolyCamera](/components/poly-camera)** for the full prop table, defaults, and usage patterns. ## Polygon Data Model Each polygon is a plain object. The only required field is `vertices` (three or more `[x, y, z]` points in world space): ```ts interface Polygon { vertices: [number, number, number][]; // Required: 3+ [x, y, z] points in world space color?: string; // Hex or rgb()/rgba() only — "#f97316" texture?: string; // Image URL for UV-mapped face material?: PolyMaterial; // Shared texture material uvs?: [number, number][]; // UV coordinates (one per vertex) data?: Record; // Reflected as data-* DOM attributes } ``` Because polygons are plain objects, you can generate them from loops, load them from parsers, or compute them from any data source. ### World coordinate convention PolyCSS world space: **+X right, +Y forward (into screen), +Z up**. The camera's default `rotX=65, rotY=45` gives a classic isometric angle. Parsers (`parseObj`, `parseGltf`, `parseStl`) normalize imported coordinates to this convention. ### `(0,0,0)` origin and `autoCenter` Scene content renders relative to the (0,0,0) origin. Most mesh files are authored with the model at an arbitrary offset. Use `autoCenter` (vanilla: `auto-center`) on the mesh element to shift the mesh's bounding-box center to the origin before applying your `position` offset: ```html ``` ```tsx // React ``` ## Authoring Polygons If you generate geometry in code — an architectural kit, a procedural terrain, a shape library — the constraints below are load-bearing, and violating them fails silently rather than throwing. How much cleanup you get for free depends on **which parser** produced the mesh and **which entry point** you hand it to. Neither is uniform: | Source | Winding treatment | |---|---| | STL | Repaired. Connectivity orients closed components outward; open components follow a consistent supplied-normal signal. | | `.vox` | Correct by construction — faces are generated CCW-from-outside. | | OBJ | **Preserved as authored.** A file wound inconsistently stays that way. | | glTF / GLB | **Preserved as authored.** `doubleSided` materials emit reversed duplicate triangles. | Every parser fits the mesh to its target size and normalizes into PolyCSS's Z-up coordinates, but the axis transform is per-format: OBJ and glTF/GLB apply the cyclic permutation `(x,y,z) → (z,x,y)` to bring their +Y-up convention to +Z-up (glTF can opt out with `upAxis: "z"`); STL defaults to identity axes, the common CAD export convention, with the permutation as opt-in; `.vox` is already Z-up and only rotates the horizontal plane. Where a permutation is applied it is cyclic rather than a y↔z swap, precisely so it never flips handedness. `normalizePolygons` — which drops degenerate polygons, strips mismatched `uvs`, replaces unparseable colors with `#cccccc`, and **fan-triangulates non-coplanar n-gons** — runs on only one path: - **React/Vue `` normalizes.** Your non-coplanar quad comes back as triangles. It records warnings, but they are not surfaced to the console, so the repair is silent. - **`scene.add(...)`, ``, ``, and `` do not.** The first three run the mesh optimizer instead (or nothing, with `merge: false`); `` runs neither — it renders the polygon exactly as passed. On all of them a non-coplanar n-gon is flattened onto its average plane when its local 2D basis is built — opening cracks against its neighbours — and degenerate polygons vanish without a trace. If you generate geometry, the portable move is to emit triangles or genuinely coplanar n-gons, or to call `normalizePolygons` yourself and inspect the warnings it returns. ### Winding determines visibility Vertex order sets the face normal by the right-hand rule, and PolyCSS **backface-culls** every leaf. A polygon wound the wrong way is invisible from the side you meant to show, and its Lambert shading is computed from the flipped normal — typically ambient-only, since the directional term clamps at zero (it darkens, it does not invert). Winding affects shadows too, and differently per path: React/Vue's ground-shadow fallback projects every polygon with no orientation test, so a reversed face still casts there. The `receiveShadow` path — all renderers, and vanilla's only mechanism — light-back-face-culls caster polygons (except self-shadow and unreliable-silhouette casters), so reversing an open face's winding can remove its shadow as well as the face. Vertices are **counter-clockwise seen from the outside** (the side you want to look at): ```ts // Faces +Z (up). CCW when viewed from above. const floor = { vertices: [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], color: "#d8d2c7", }; // The SAME quad reversed faces -Z (down) and is invisible from above. const broken = { vertices: [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]], color: "#d8d2c7", }; ``` The normal is `(v1 - v0) × (v2 - v0)`, normalized. For the first quad that is `(1,0,0) × (1,1,0) = (0,0,1)` — pointing up, so the face is visible from above. Consequences worth internalizing when you build a shape library: - **Solids face outward; rooms face inward.** A box you look at from outside and a room you stand inside are the same six quads with opposite winding. - **Mirroring reverses handedness.** Any negative scale or mirror transform flips the effective winding of every face it touches, so mirrored geometry needs its vertex order reversed to compensate. - **Reverse UVs with the vertices.** If a polygon has `uvs`, reversing `vertices` without reversing `uvs` in the same order silently remaps the texture. - **`doubleSided` is not the fix.** It exists so importers can stop the optimizer collapsing intentional reverse-wound faces; it is not a render-time flag and will not make a face visible from behind. Orient the winding. **Diagnostic rule:** a single-sided face is *supposed* to disappear when the camera moves behind it — that alone is not a bug. The winding symptom is a surface missing or flickering **from the viewpoint it was built to be seen from**: the polygon exists in your data, its neighbours render, but the face only shows from the opposite side. When you see that, inspect the face's winding and normal *before* you touch culling, lighting, or camera code. ### `color` is not a full CSS color Only hex (`#rgb`, `#rrggbb`) and `rgb()` / `rgba()` parse. CSS named colors (`"tomato"`, `"red"`), `hsl()`, and `color()` do **not**, and the failure is silent either way: on the normalizing `` path the value is replaced with `#cccccc`, and everywhere else it renders **white**. ```ts { color: "tomato" } // ✗ silently wrong (white, or #cccccc) { color: "#ff6347" } // ✓ ``` ### Non-triangular polygons must be coplanar On every path except ``, a quad or n-gon whose vertices are not on a common plane is flattened onto their average plane when its local 2D basis is built, which moves vertices out from under their neighbours and opens visible cracks. `` instead fan-triangulates it, which avoids the crack but silently changes your topology. Triangles are always coplanar, so this only bites n-gons. If you merge faces into quads, either merge only where coplanarity genuinely holds, or snap the shared vertices onto a common plane and propagate the new position to **every** polygon that references them. ### The optimizer rewrites your geometry by default `merge` defaults to `true` and `meshResolution` defaults to `"lossy"`, so authored geometry is merged, deduped, and interior-culled before it renders: - Coincident faces closer than `0.05` world units are deduped. - Fully-interior faces are culled. - Lossy merging starts at up to `0.35` world units of plane displacement and `0.04` of boundary displacement, at up to `15°` of angle change. **These are not the maximum.** When the default pass doesn't pay off, the optimizer also tries progressively more aggressive variants at `30°`, `45°`, and `60°` — the widest also raising boundary displacement to `0.06`. Those are accepted only on a material render-cost win with non-worsening seam diagnostics, but they *can* apply to your geometry. The degree values are angular thresholds; the plane and boundary displacement budgets are **absolute world units**. None of them are configurable, so small-scale hand-authored meshes can get visibly welded. Note that dedupe and interior culling are treated as *exact* reductions, so they still run under `meshResolution: "lossless"` — only the lossy approximation is switched off. **`merge: false` renders the polygon array you pass completely untouched — but only on the paths that accept it**: vanilla `scene.add(...)` and React/Vue ``. It does not exist on `` (always normalized and merged) or on the `` custom element. And it cannot undo `loadMesh`'s own parse-time optimization — geometry loaded from a file is deduped and optimized *before* `merge` is ever consulted. There is **no fully identity path for file geometry**: the parsers themselves normalize their input (fit to `targetSize`, default `60`; reposition the origin; remap axes to Z-up; round coordinates; fan-triangulate n-gons and drop degenerate triangles). STL additionally repairs winding, and `.vox` synthesizes greedy-meshed quads from the voxel grid. To preserve the *direct parser output* from any further renderer optimization, call `parseObj` / `parseStl` / `parseGltf` / `parseVox` directly and add the result with `merge: false`. ### Degenerate polygons vanish silently Fewer than three vertices, a zero-area face, or a degenerate first edge produces no leaf and no console output. If a face is missing and the winding is right, check for duplicate or collinear vertices. ### Scale Camera `zoom` is **on-screen CSS pixels per world unit** — at `zoom: 50`, one world unit renders 50 px across. The default is `0.65`, and orbit controls clamp it to `0.1…10`. Internally, renderer geometry already lives at `BASE_TILE` (50) CSS px per world unit, and the camera transform divides that back out (`scale(zoom / 50)`), so the two cancel. You only need `BASE_TILE` when converting world units to raw CSS px yourself — for example ``, which is in world units and mounts a document `width × 50` CSS px wide. ## Rendering Pipeline PolyCSS is structured in three layers: 1. **Core** (`@layoutit/polycss-core`): Pure math and parsing. Handles OBJ / STL / glTF / GLB / VOX parsing, UV decoding, lighting math, and polygon normalization. No DOM dependency. 2. **DOM renderer**: Takes parsed polygons and produces one leaf DOM element per visible polygon. The renderer prefers CSS primitives for solid quads, triangles, and clipped solids, then falls back to atlas slices for textures or unsupported shapes. Atlas canvas work is one-shot; camera, mesh, and dynamic-light updates use transforms and CSS custom properties. 3. **Entry points**: The vanilla `@layoutit/polycss` package exposes custom elements (``, ``, ``, ``, controls, helpers, shapes) plus imperative APIs such as `createPolyCamera`, `createPolyScene`, `createSelect`, and `createTransformControls`. React (`@layoutit/polycss-react`) and Vue (`@layoutit/polycss-vue`) bindings mirror that surface with framework-native reactivity, lifecycle, and prop updates. ## Render Strategies The internal leaf tag is a strategy, not public API: | Tag | Strategy | Typical use | |-----|----------|-------------| | `` | Solid quad | Axis-aligned rectangles and stable projective quads. | | `` | Stable triangle / corner-shape solid | Solid triangles and exact beveled-corner solids. | | `` | Border-shape clipped solid | Solid non-rect polygons on browsers with `border-shape`. | | `` | Atlas slice | Textured polygons and fallback solids. | You normally do not target these tags directly; use `Poly`, `PolyMesh`, classes, data attributes, or render stats. Cast shadows are separate SVG shadow surfaces, not render-strategy leaves; meshes with `castShadow` project onto `receiveShadow` surfaces in both lighting modes (React/Vue additionally fall back to the scene ground plane when no receiver exists — vanilla does not). ## Automatic Polygon Merge Before rendering, PolyCSS automatically optimizes loaded meshes. `meshResolution: "lossless"` keeps exact planar candidates only; the default `"lossy"` mode also bakes solid texture swatches, merges visually redundant baked swatch colors, tries static triangle simplification for eligible non-animated imports, and can merge near-coplanar candidates within a bounded displacement budget. Candidates are accepted only when the final DOM win is meaningful and whole-mesh seam diagnostics do not regress. STL **parsing** is conservative — the import pass uses the lossless optimizer and skips ray-based interior culling, because public CAD/STL files often contain shell, winding, or topology quirks. That protection is parse-time only: the renderer's own optimization pass does not know the geometry came from STL, so it interior-culls and, by default, lossy-merges. Preserve the parsed STL surface with `merge: false`. This keeps DOM element counts low for flat surfaces without changing the intended rendered shape. Per-polygon DOM identity is preserved for polygons that cannot merge; polygons inside a merged flat region become one rendered element. ## Related - [Quickstart](/quickstart): Install + first scene walkthrough. - [PolyCamera](/components/poly-camera): Camera props reference. - [PolyScene](/components/poly-scene): Scene props and polygon data reference. - [Performance](/guides/performance): DOM tuning and parser options. --- # Animation import { Tabs, TabItem } from '@astrojs/starlight/components'; PolyCSS can play usable glTF / GLB animation clips by sampling them into polygon frames. The parser exposes `ParseResult.animation`; `usePolyAnimation` and `createPolyAnimationMixer` drive a mesh handle over time. ## How It Works Animation in PolyCSS is a polygon-frame pipeline: the parser turns glTF / GLB animation data into a sampler, and the renderer receives ordinary `Polygon[]` frames. When `loadMesh()` or `parseGltf()` finds usable animation clips, the returned `ParseResult.animation` exposes clip metadata and a `sample()` function. Sampling evaluates the source animation at a given time, applies the animated pose to the mesh, and returns polygons for that moment. `usePolyAnimation` and `createPolyAnimationMixer` sit on top of that sampler. They manage actions, looping, playback speed, fades, and cross-fades, then apply each sampled frame to a mesh handle. ## Usage Use the core mixer directly. The mesh handle returned by `scene.add()` satisfies `PolyAnimationTarget`. ```ts import { createPolyCamera, createPolyScene, createPolyAnimationMixer, loadMesh, } from "@layoutit/polycss"; const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(host, { camera }); const result = await loadMesh("/character.glb", { meshResolution: "lossless" }); const mesh = scene.add(result, { merge: false, stableDom: true }); if (result.animation?.clips.length) { const mixer = createPolyAnimationMixer(mesh, result.animation); mixer.clipAction(result.animation.clips[0].name).reset().play(); let last = performance.now(); function tick(now: number) { mixer.update((now - last) / 1000); last = now; requestAnimationFrame(tick); } requestAnimationFrame(tick); } ``` `usePolyAnimation` mirrors drei's `useAnimations`: it returns `clips`, `names`, `actions`, `mixer`, and a `ref`. Load the mesh yourself when you need access to both `polygons` and the parser's animation controller. ```tsx import { useEffect, useRef, useState } from "react"; import { PolyCamera, PolyScene, PolyMesh, PolyOrbitControls, loadMesh, usePolyAnimation, type ParseResult, type PolyMeshHandle, } from "@layoutit/polycss-react"; export function AnimatedModel() { const [result, setResult] = useState(null); const meshRef = useRef(null); const { actions, names } = usePolyAnimation( result?.animation?.clips, result?.animation, meshRef, ); useEffect(() => { let cancelled = false; let active: ParseResult | null = null; loadMesh("/character.glb").then((next) => { active = next; if (cancelled) next.dispose(); else setResult(next); }); return () => { cancelled = true; active?.dispose(); }; }, []); useEffect(() => { const first = names[0]; if (!first) return; actions[first]?.reset().play(); }, [actions, names]); return ( {result && ( )} ); } ``` The Vue composable exposes the same concepts as the React hook, but `clips`, `names`, `actions`, and `mixer` are Vue computed refs. ```vue ``` ## Practical Notes - `usePolyAnimation` owns its `requestAnimationFrame` loop. Vanilla callers own the loop and call `mixer.update(deltaSeconds)` themselves. - `usePolyAnimation` and the core mixer expose familiar action methods: `play`, `stop`, `reset`, `fadeIn`, `fadeOut`, `crossFadeTo`, `setLoop`, `setEffectiveTimeScale`, and `setEffectiveWeight`. - Cross-fading assumes the sampled clips share matching polygon counts and vertex order. That is true for clips from the same parsed mesh. - `LoopOnce`, `LoopRepeat`, and `LoopPingPong` match the three.js numeric constants. - `dispose()` still matters: parser-created blob URLs should be revoked when the model is no longer used. ## Related - [Loading Meshes](/guides/textures): Parser options and mesh lifecycle. - [Core Types](/api/types): `ParseAnimationController`, `PolyAnimationMixer`, and clip types. - [Performance](/guides/performance): Why skeletal animation is the render-loop exception. --- # Lighting & Shadows import { Tabs, TabItem } from '@astrojs/starlight/components'; import PolyDemo from '../../../components/PolyDemo.astro'; PolyCSS shades each polygon with a Lambert model that matches three.js's `MeshLambertMaterial`. A scene takes **one directional light, one ambient light, and any number of point lights**, set on `` / `PolyScene` / `createPolyScene()`. Shading happens either once on the CPU (**baked**) or live in CSS `calc()` (**dynamic**) — see [Lighting modes](#lighting-modes). The light types are documented in the [Core Types reference](/api/types/#polydirectionallight); this guide is the conceptual tour. ## The light sources ### Directional light One infinitely-distant light with a single `direction`, `color`, and `intensity` — like the sun. Every surface gets `color · intensity · max(0, n · L̂)`. Drag **light °** / **light ↑** to move the sun and watch the lit side — and the cast shadow on the ground — follow; **intensity** and **ambient** control brightness and fill. ```js import { createPolyScene, createPolyCamera } from "@layoutit/polycss"; const scene = createPolyScene(host, { camera: createPolyCamera({ rotX: 60, rotY: 30 }), directionalLight: { direction: [0.5, -0.6, 0.6], color: "#ffffff", intensity: 1 }, ambientLight: { intensity: 0.3 }, }); ``` ```tsx import { PolyCamera, PolyScene } from "@layoutit/polycss-react"; {/* meshes */} ``` ```vue ``` ### Ambient light A uniform fill with `color` and `intensity` (default `0.4`) added to every surface regardless of orientation. Use it to lift shadowed faces out of pure black — exactly what a shadowed region fades toward. ### Point lights Positional lights given as an array. Each has a world-space `position`, `color`, `intensity`, and optional `castShadow`. Point lights are **direction-only** — there is **no distance falloff** (they emulate three.js's `PointLight(distance: 0, decay: 0)`); a surface is lit by `color · intensity · max(0, n · L̂)` where `L̂` points from the surface to the light. Shading is flat per face — an accepted approximation of three.js's per-fragment gradient, exact for small faces or distant lights. > **Point lights are baked-mode only.** Dynamic mode's zero-JS light updates can't express a per-face direction that varies with position, so dynamic scenes ignore `pointLights` entirely (shading *and* shadows). See [Lighting modes](#lighting-modes). ```js const scene = createPolyScene(host, { camera: createPolyCamera({ rotX: 35, rotY: 20 }), // textureLighting defaults to "baked" — required for point lights. pointLights: [ { position: [-4, 4, 5], color: "#ff7755", intensity: 1, castShadow: true }, { position: [5, -3, 4], color: "#5599ff", intensity: 1, castShadow: true }, ], ambientLight: { intensity: 0.3 }, }); ``` ```tsx {/* meshes */} ``` ```vue ``` ## Lighting modes `textureLighting` (scene-level, with a per-mesh override) chooses **how** the Lambert result is applied: | | **`"baked"`** (default) | **`"dynamic"`** | |---|---|---| | How | Computed once on the CPU, written into each leaf's color / atlas pixels | Resolved live in CSS `calc()` from scene-root variables + per-leaf normals | | Point lights | ✅ Supported | ❌ Ignored (direction-only CSS can't vary per position) | | Moving the directional light | Re-bake needed for the lit surface (shadows update for free) | **Zero JS** — just updates a CSS variable | | Best for | Static scenes, point lights, exact color | Live / animated directional light, interactive light dragging | Rule of thumb: **dynamic** when the directional light moves every frame; **baked** when you want point lights or the lights are mostly static. ## Cast shadows Shadows are CPU-projected SVG surfaces (not a render-strategy leaf). Mark casters with `castShadow` and receivers with `receiveShadow`: ```js scene.add(parsedFloor, { receiveShadow: true }); scene.add(parsedModel, { castShadow: true }); scene.setOptions({ shadow: { color: "#000000", opacity: 0.3, lift: 0.02 } }); ``` ```tsx ``` ```vue ``` ### Two ways to receive a shadow There are two distinct receiver mechanisms, and mixing them up is a common source of "my shadow disappeared": - **Ground-shadow fallback — React/Vue only.** A `castShadow` mesh projects onto the scene's ground plane automatically, with no receiver mesh required. This is what `` relies on: it is a convenience quad that renders with `castShadow: false` and has **no `receiveShadow` prop of its own** (passing one does nothing). **Vanilla `createPolyScene` has no such fallback** — a caster with no receiver in the scene emits no shadow, so vanilla scenes must mark a floor mesh `receiveShadow: true` explicitly. - **`receiveShadow` meshes — all renderers.** Marking any mesh `receiveShadow` makes casters project per-coplanar-face SVG shadows onto each of its visible surfaces (Three.js `mesh.receiveShadow` semantics). In React/Vue, as soon as *any* receiver exists in the scene, casters **drop the ground-shadow fallback** so the receiver paints the only shadow pass. So in React/Vue, `` alone works and an explicit `receiveShadow` mesh alone works — but adding a receiver elsewhere silently turns off the ground fallback under ``. In vanilla, an explicit receiver is the only option. What to expect: - **Directional shadows** work in both lighting modes and only appear for a directional light with `intensity > 0` (intensity `0`, or no directional light, casts nothing — matching three.js). - **Point-light shadows** are baked-mode only and **radial** (each vertex projects along its own ray from the light). Set `castShadow: true` on the point light *and* `castShadow`/`receiveShadow` on the meshes. - **Colored multi-light shadows.** Each light's shadow is filled with the receiver lit by every *other* light, so a spot blocked from one colored light still shows the others' color. Where two shadows overlap, the region composites to the both-blocked color (ambient only) — not a doubled-up black smear. - **`shadow.lift`** floats the shadow a hair above the receiver to avoid z-fighting; the default is fine, just don't set it to exactly `0` on a coplanar floor. ## Parametric shadows By default a shadow projects every casting polygon. Set `shadow: { parametric: true }` and PolyCSS instead casts one low-resolution **coverage silhouette** per caster — far fewer DOM/SVG vertices and a cheaper projection, at the cost of an approximate (but concave- and hole-aware) outline. The exact path stays the default; parametric is purely opt-in and works in all three renderers. ```js // vanilla scene.setOptions({ shadow: { parametric: true, definition: 32 } }); ``` ```jsx // React ``` ```vue ``` - **`definition`** (default `16`) — silhouette detail. Higher → closer to the exact outline and sharper holes, but more vertices. Override it per mesh with **`shadowDefinition`** (`PolyMeshTransform.shadowDefinition` in vanilla, `` in React, `shadow-definition` in Vue) so a detailed hero caster stays crisp while simple props run cheap in the same scene. - **`style: "vector" | "pixel"`** — `"vector"` (default) traces a smooth concave contour; `"pixel"` greedy-meshes the coverage into blocky **voxel** rectangles, where `definition` becomes the pixel-grid resolution (lower → chunkier). Holes (courtyards, an arena) come through in both. - **Point lights** are supported — each casting point light gets its own radial silhouette. - **`dragDefinition`** *(vanilla `createPolyScene` only)* — progressive refinement. While the directional light is being dragged the shadow renders at this lower definition for a smooth drag, then a debounced pass re-emits at full `definition` once the light settles. React/Vue get the same effect by lowering `definition` in component state during a drag. - **`followAnimation`** — by default a deforming/animated caster's shadow **freezes** at the last pose (re-projecting it every frame is expensive). Set `followAnimation: true` to make the shadow track the animation; pair it with `parametric` + a modest `definition` so each reprojection stays cheap. All three renderers throttle the follow re-emit to the same 80 ms window (~12fps, leading + trailing edge), so a paused animation still lands its final pose. ## Live & animated lights - **Animating the directional light:** use **dynamic** mode — moving the light is a single CSS-variable write per frame, no JS in the paint loop, and shadows re-project automatically. - **Baked mode + a light change:** cast shadows re-emit automatically (cheap SVG), but the baked *lit surface* stays frozen until you re-bake — call `mesh.rebakeAtlas()` (debounce it to the end of a drag; the atlas raster is the one costly step). The React/Vue components re-bake automatically on prop change, so this only applies to the imperative `createPolyScene()` API. - **Animating point lights:** baked-mode only, so re-bake per change (or per debounced step). For continuously moving lights, prefer a single directional light in dynamic mode. --- # PolyCSS Morph import MorphTargetsDemo from "../../../components/MorphTargetsDemo.astro"; `@layoutit/polycss-morph` is an imperative, framework-agnostic layer for models whose geometry and paint resources are built ahead of time, then updated through a stable DOM graph. ```bash npm install @layoutit/polycss-morph ``` Use the regular `Polygon[]` path when a mesh does not need prepared updates. Use Morph when a model needs stable DOM elements plus morph targets, semantic controls, springs, joint skinning, or prepared playback. ## Package entries The Node and browser dependency graphs are separate: | Entry | Environment | Purpose | |---|---|---| | `@layoutit/polycss-morph/prepare` | Node | Turn glTF/GLB sources into deterministic model packages with CSS triangle plans and browser fallbacks. | | `@layoutit/polycss-morph` | Browser or shared code | Validate, load, mount, and update prepared models. | The Node preparer creates `static-prepared` and `morph-regions` models. Custom tooling can create validated `joint-skin` and `prepared-playback` models. ## Prepare ```ts import { preparePolyMorphModel } from "@layoutit/polycss-morph/prepare"; await preparePolyMorphModel({ configPath: "./source/prepare.json", outputRoot: "./public/model/package", }); ``` Preparation writes resources before `manifest.json`. Re-running with `check: true` verifies the complete existing inventory and bytes without rewriting them. ## Load, mount, update ```ts import { createPolyMorphDeformationRuntime, loadPolyMorphPackage, mountPolyMorphModel, } from "@layoutit/polycss-morph"; const loaded = await loadPolyMorphPackage("/model/"); const mounted = mountPolyMorphModel(host, loaded.model, { resources: loaded.resources, }); const deformation = createPolyMorphDeformationRuntime(loaded.model); const frame = deformation.sample({ tick: 0, morphWeights: { "pin-c06-r04-lift": 0.5 }, }); mounted.apply({ leaves: frame.leafUpdates }); ``` The same imperative API is used from vanilla, React, or Vue applications. Morph does not ship framework wrappers. Prepared playback commits explicitly: apply `sample.update`, then call `runtime.commit(sample)` only after `mounted.apply(...)` succeeds. If an application already owns a retained DOM graph, `createPolyMorphPreparedDomTarget` adopts its source-ordered model, shape, and leaf elements as indexed write targets. Morph deduplicates requested values and invalidates the writers at destroy; the application retains DOM teardown ownership. ## Example: Cube to sphere This 768-polygon model from the [three.js morph-target example](https://github.com/mrdoob/three.js/blob/7763535f6f944a32105af7d9d57d83a92f310d51/examples/webgl_morphtargets.html) exposes two prepared targets. **Spherify** moves the cube toward a sphere; **Swirl** applies the source twist target. Both sliders may be combined while the same DOM leaves remain mounted. Drag anywhere on the stage to orbit the model. The page samples only when a slider changes. Geometry follows the slider directly; releasing it commits the nearest prepared lighting state with at most two ancestor custom-property writes: ```ts const deformation = createPolyMorphDeformationRuntime(model); const mounted = mountPolyMorphModel(host, loaded.model, { resources: loaded.resources, }); function update(spherify: number, twist: number) { const frame = deformation.sample({ tick: tick++, morphWeights: { spherify, twist }, }); mounted.apply({ leaves: frame.leafUpdates }); } ``` Morph owns no scheduler. Applications decide whether weights come from direct input, state, an animation sampler, or a spring. Sparse deformation supports retained solid triangles and affine solid quads. Planar projective solid quads are available where PolyCSS enables projective quad compositing. Deformation rejects non-coplanar, non-convex, or compositor-unstable geometry, and the retained mount rejects projective quad matrices on unsupported Safari-family browsers before DOM writes. ## Profiles | Profile | Behavior | |---|---| | `static-prepared` | Retained rendering with no deformation. | | `morph-regions` | Sparse morph targets, semantic controls, springs, and clip channels. | | `joint-skin` | Hierarchical joint transforms and normalized weighted skinning. | | `prepared-playback` | Source-ordered retained model, shape, transform, visibility, opacity, and image-row changes. | ## Runtime behavior Mount once, then sample and apply: - the application owns input and timing; - Morph owns no animation scheduler; - leaf elements retain identity until teardown; - unchanged samples perform no writes; - sparse samples visit only affected leaves; - runtime updates do not reconstruct topology or redraw prepared image resources. Morph chooses the triangle paint path once when it mounts. It uses `corner-shape` where available, a larger CSS border triangle in Firefox, and each leaf's prepared polygon-sized alpha-atlas slice in WebKit/Safari. Mount uses the loader's verified image bytes and revokes their object URLs at teardown; the browser never refetches, generates, or redraws those pages. ## Application ownership Morph owns the prepared model format and sparse DOM updates. Your application owns input, timing, presentation, model-specific preparation, and product behavior. --- # Performance import PolyDemo from '../../../components/PolyDemo.astro'; PolyCSS renders meshes as real DOM elements: every visible polygon is an HTML leaf with a CSS transform. That gives you DevTools inspection, DOM events, and CSS styling on every polygon, but it means **performance scales with mounted element count and atlas area**. Understanding both is key to building smooth scenes. ## Live demo: polygon count vs. performance The sphere below starts at subdivision level 3 (320 triangles). Bump subdivisions up to 4 (1,280) or 5 (5,120) and watch the frame rate drop. This illustrates how quadrupling the subdivision level quadruples the DOM node count: and rendering cost. ## Polygon count matters The dominant cost in PolyCSS is usually browser work over the DOM tree: style, layout, paint, and compositing over many transformed leaves. Camera and mesh motion are expressed as ancestor transforms where possible, so the hot path avoids per-polygon JavaScript, but the browser still has to process every mounted leaf. Confirmed during benchmarking on a 10k-triangle mesh with auto-rotate: - **Scripting**: ~579ms / 7s (mostly React re-renders) - **Rendering (style recalc + layout)**: ~2,130ms / 7s: the dominant cost ## Automatic mesh optimization PolyCSS automatically optimizes loaded meshes before rendering. The default `meshResolution: "lossy"` path bakes solid texture swatches, merges visually redundant baked swatch colors, tries static triangle simplification for eligible non-animated imports, merges compatible polygons, and can use bounded geometric approximation when that lowers estimated DOM render cost. Wider lossy candidates are gated by whole-mesh seam diagnostics and a minimum render-cost win. `meshResolution: "lossless"` keeps exact planar candidates only. This is most useful for architectural meshes with large flat surfaces: walls, floors, ceilings, voxel faces, and other areas where many same-material triangles can collapse without visual change. **Limitations:** - Per-polygon DOM addressing is lost for merged groups. - UV-textured polygons only merge when texture mapping can be preserved. Lossless mode requires exact coplanarity; lossy mode may project near-coplanar textured neighbors into one atlas sprite within the configured displacement budget. - Per-polygon DOM addressing is not available inside a merged flat region. ## Render strategies Simple polygons are cheaper than textured or irregular polygons. The renderer uses CSS primitives where possible: - Axis-aligned rectangles and stable quads use solid `` leaves. - Solid triangles and exact corner-shape solids use `` leaves when supported. - Other supported solid clipped polygons use `` leaves. - Textured polygons and fallbacks use atlas `` leaves. - Cast shadows use SVG shadow surfaces for meshes with `castShadow`; light changes reproject the path without atlas redraw. Use `collectPolyRenderStats(root)` to inspect the mounted leaf mix. For diagnostics, `strategies={{ disable: ["b", "i", "u"] }}` forces fallback atlas rendering so you can compare output or isolate browser compositor bugs. ## Voxel fast paths Voxel-shaped meshes are special. Generic voxel-shaped polygon meshes can mount only camera-facing normals and patch the mounted set as the camera or mesh rotation crosses a face boundary. Raw `.vox` sources also preserve `voxelSource`; eligible baked-mode meshes in vanilla, React, and Vue render visible voxel quads directly as `` leaves inside face wrappers. Dynamic lighting, shadows, animation, non-exact voxel geometry, or geometry replaced via `setPolygons()` fall back to the polygon renderer. ## `targetSize` and polygon count Use `targetSize` when parsing to keep models at a predictable world-space scale. This does not decimate the source mesh by itself, but it affects how large the generated DOM geometry and atlas footprints are. For real element-count reduction, rely on automatic coplanar merge, interior culling, or lower-poly source assets. ```ts // Vanilla imperative loading const result = await loadMesh("/castle.obj", { objOptions: { targetSize: 30 }, }); ``` ```tsx // React ``` ## `textureQuality` and texture memory Generated atlas pages default to `textureQuality="auto"`. Auto starts from the packed atlas area, caps oversized runtime bitmaps by page side length and decoded-memory budget, and chooses the fixed CSS sprite size used by atlas leaves. Desktop-class auto uses a 128px sprite to avoid Safari/Firefox compositor flattening artifacts; mobile-class auto and explicit numeric quality use 64px to keep layer memory lower. ```html ``` ```tsx // React // Or per mesh: ``` Use explicit numeric values when you want to override auto raster scale: `0.5` or `0.75` for distant or dense assets, `1` for close-up inspection when the runtime bitmap cost is acceptable. Numeric quality keeps the 64px atlas sprite size. ## Atlas and Blob URL Lifecycle UV-textured polygons share generated atlas blob URLs (created during the canvas rasterization pass at mount). These are revoked on unmount. Keep these rules in mind: - Textured meshes do a one-time atlas generation pass at mount; very large texture footprints can still cost memory and startup time. - `solidTextureSamples` can convert uniform texture swatches into solid-color polygons before optimization, avoiding unnecessary atlas slices for low-poly assets that use a texture atlas as a color palette. - Call `dispose()` on `ParseResult` (or `usePolyMesh` result) when you've loaded the mesh imperatively. - The mesh element (`` / ``) handles disposal automatically on unmount or `src` change. ## Related - [PolyScene](/components/poly-scene): Scene props reference. - [Loading Meshes](/guides/textures): Parsing options including `targetSize`. - [Core Concepts: Automatic Polygon Merge](/core-concepts#automatic-polygon-merge): Conceptual overview. --- # Projections import { Tabs, TabItem } from '@astrojs/starlight/components'; import PolyDemo from '../../../components/PolyDemo.astro'; PolyCSS uses CSS 3D perspective to project the scene. Camera attributes (`rot-x`, `rot-y`, `zoom`, `perspective`) live on the wrapping camera element — `` for orthographic (default) or `` for perspective — never on ``. `PolyCamera` is orthographic by default; pick `PolyPerspectiveCamera` when depth foreshortening is needed. ## Live demo: camera controls Use the sliders below to explore how `perspective`, `rotX`, and `rotY` interact. ## Perspective depth Higher `perspective` values feel flatter (more isometric-like); lower values exaggerate depth. For orthographic projection (no perspective distortion, the default), use `` / `PolyCamera`. For perspective distortion, use `` / `PolyPerspectiveCamera`. ```html ``` ```tsx import { PolyCamera, PolyPerspectiveCamera, PolyScene, PolyIcosahedron } from "@layoutit/polycss-react"; // Orthographic (default) // Standard perspective // Very flat / near-isometric perspective ``` ```vue ``` ## Camera angles Use `rot-x` and `rot-y` on the camera element to position the camera: - `rotX`: vertical tilt. `90` is straight-down top view; `0` is horizontal. - `rotY`: horizontal rotation (0–360). Controls which face of the scene is forward. ```html ``` ```tsx // React ``` ## When to adjust perspective - **High perspective (≥ 4000):** Near-isometric feel, minimal depth distortion. Good for architectural renders or strategy-game-style views. - **Low perspective (500–1000):** Strong depth foreshortening. Good for dramatic close-up shots or first-person-adjacent views. - **Orthographic (`PolyOrthographicCamera`):** Perfect isometric, no depth distortion at all. Good when you need pixel-perfect isometric scenes. ## Related - [PolyScene](/components/poly-scene): Scene rendering and lighting reference. - [PolyCamera](/components/poly-camera): Full `perspective`, `rotX`, `rotY` prop reference. - [Performance](/guides/performance): Merge modes and DOM tuning. --- # Per-polygon Interaction import { Tabs, TabItem } from '@astrojs/starlight/components'; import PolyDemo from '../../../components/PolyDemo.astro'; Every polygon rendered by PolyCSS is a real DOM element. You can attach standard event handlers, apply CSS classes, and inspect them in DevTools: that's true regardless of which entry point you use. The single-polygon primitive is `` (vanilla custom element) / `` (React, Vue). ## Live demo: interactive scene ## Box helper Use `boxPolygons()` when your shape is an axis-aligned box or cuboid. It returns ordinary `Polygon[]`, so the result renders through the same `` / `createPolyScene()` path as parsed OBJ, STL, GLB, and VOX meshes. ```ts import { boxPolygons } from "@layoutit/polycss-react"; const polygons = boxPolygons({ min: [0, 0, 0], max: [2, 1, 0.5], color: "#d8d2c7", data: { tileId: "tile-1" }, faces: { top: { texture: "/tile.png", data: { face: "top" } }, bottom: false, }, }); ``` ## Primitive shape exports PolyCSS ships built-in shape components for common primitives. React and Vue export `PolyBox`, `PolyPlane`, `PolyRing`, `PolyOctahedron`, `PolySphere`, `PolyTetrahedron`, `PolyIcosahedron`, `PolyDodecahedron`, `PolyCylinder`, `PolyCone`, and `PolyTorus`. The vanilla package exports matching ParseResult factories: `createPolyBox`, `createPolyPlane`, `createPolyRing`, `createPolyOctahedron`, `createPolySphere`, `createPolyTetrahedron`, `createPolyIcosahedron`, `createPolyDodecahedron`, `createPolyCylinder`, `createPolyCone`, and `createPolyTorus`. Shape components accept their geometry options plus the common mesh props (`position`, `scale`, `rotation`, `autoCenter`, `id`, and event props where supported). When you need raw polygon arrays, use the core generators directly: `boxPolygons`, `planePolygons`, `ringPolygons`, `octahedronPolygons`, `spherePolygons`, `tetrahedronPolygons`, `icosahedronPolygons`, `dodecahedronPolygons`, `cylinderPolygons`, `conePolygons`, `torusPolygons`, `axesHelperPolygons`, and `arrowPolygons`. ```tsx import { PolyCamera, PolyScene, PolyBox, PolySphere, PolyTorus } from "@layoutit/polycss-react"; ``` ## The polygon primitive `` (vanilla) and `` (React / Vue) render a single polygon as one internal leaf element. The renderer picks the cheapest strategy for that polygon: solid CSS primitives where possible, atlas slices for textured or irregular faces. They forward standard DOM props (`onclick`, `class`, `style`, `aria-*`, etc.). ```html ``` ```tsx import { PolyCamera, PolyScene, Poly } from "@layoutit/polycss-react"; import type { Vec3 } from "@layoutit/polycss-react"; const triangle: Vec3[] = [[0,0,0], [1,0,0], [0,1,0]]; ``` ## Interactive per-polygon example ```html ``` ```tsx import { useState } from "react"; import { PolyCamera, PolyScene, Poly } from "@layoutit/polycss-react"; import type { Polygon } from "@layoutit/polycss-react"; export function InteractiveMesh({ polygons }: { polygons: Polygon[] }) { const [hoveredId, setHoveredId] = useState(null); return ( {polygons.map((p, i) => ( alert(`clicked polygon ${i}`)} onMouseEnter={() => setHoveredId(i)} onMouseLeave={() => setHoveredId(null)} className={hoveredId === i ? "highlight" : ""} style={{ transition: "filter 0.2s" }} /> ))} ); } ``` ```css /* highlight.css */ .highlight { filter: brightness(1.5); } ``` ```vue ``` ## Shared materials Use `material` when multiple polygons share the same texture identity. React and Vue export `usePolyMaterial` to keep that material object stable across rerenders, which is useful with memoized polygon lists or `` children. ```tsx import { usePolyMaterial, Poly } from "@layoutit/polycss-react"; const material = usePolyMaterial({ texture: "/stone.png", key: "stone" }); ; ``` ```vue ``` ## Per-polygon override (mesh + custom render) To customize specific polygons inside a loaded mesh, use: - **Vanilla:** load with `loadMesh`, then add one mesh handle per polygon. You stay in full control of which polygons get special handling. - **React:** the `` render-prop child. - **Vue:** the `` scoped slot. ```ts import { loadMesh, createPolyCamera, createPolyScene } from "@layoutit/polycss"; const host = document.getElementById("scene-host"); const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(host, { camera }); const result = await loadMesh("/character.glb"); const handles = result.polygons.map((polygon, i) => { const handle = scene.add( { polygons: [polygon], objectUrls: [], warnings: [], dispose: () => {} }, { id: `polygon-${i}`, merge: false }, ); handle.element.addEventListener("click", () => { handle.element.classList.toggle("outlined"); }); return handle; }); // later: handles.forEach(handle => handle.remove()); scene.destroy(); result.dispose(); ``` ```tsx import { useState } from "react"; import { PolyCamera, PolyScene, PolyMesh, Poly } from "@layoutit/polycss-react"; export function SelectableMesh() { const [selected, setSelected] = useState(null); return ( {(polygon, index) => ( setSelected(index)} className={selected === index ? "outlined" : ""} /> )} ); } ``` ```vue ``` ## Mesh selection For whole-mesh selection, use `PolySelect` / `` instead of wiring every polygon manually. It tracks selected `PolyMeshHandle`s, supports multi-select, and exposes an imperative selection API for sidebars and transform gizmos. ```tsx import { useState } from "react"; import { PolyCamera, PolyScene, PolyMesh, PolySelect, PolyTransformControls, type PolyMeshHandle, } from "@layoutit/polycss-react"; export function SelectAndMove() { const [selected, setSelected] = useState(null); return ( setSelected(meshes[0] ?? null)}> ); } ``` Use `usePolySelect()` to read the current selection inside a React subtree and `usePolySelectionApi()` when a nested toolbar needs to call `set`, `add`, `remove`, `toggle`, or `clear`. For lower-level DOM tools, React and Vue also export `findPolyMeshHandle(el)`, `pointInMeshElement(meshEl, clientX, clientY)`, and `findMeshUnderPoint(clientX, clientY, filter?)`. They resolve rendered DOM hits back to `PolyMeshHandle`s and use the same bounding-rect fallback that selection and transform controls use for clipped polygon leaves. ## Imperative loading Load a mesh programmatically when you need control over loading state. The vanilla `loadMesh` is the universal path; React adds a `usePolyMesh` hook on top that auto-disposes on unmount. ```ts // Vanilla: works in any framework or no framework import { loadMesh, createPolyCamera, createPolyScene } from "@layoutit/polycss"; const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(document.getElementById("host")!, { camera }); const result = await loadMesh("/model.glb"); scene.add(result); // ... later: scene.destroy(); // removes the scene and disposes registered meshes ``` ```tsx // React import { PolyCamera, PolyScene, Poly, usePolyMesh } from "@layoutit/polycss-react"; function Viewer() { const { polygons, loading, error } = usePolyMesh("/model.glb"); if (loading) return ; if (error) return
Error: {error}
; return ( {polygons.map((p, i) => )} ); } ``` ## Related - [PolyScene](/components/poly-scene): Scene props and polygon data reference. - [Loading Meshes](/guides/textures): OBJ, STL, glTF, GLB, VOX, MTL loading and UV textures. - [Performance](/guides/performance): Merge modes and DOM tuning. --- # Loading Meshes import { Tabs, TabItem } from '@astrojs/starlight/components'; import PolyDemo from '../../../components/PolyDemo.astro'; PolyCSS loads 3D mesh files (OBJ, STL, glTF, GLB, and MagicaVoxel VOX) and renders them as DOM elements. UV textures are extracted from OBJ/glTF/GLB files and packed into generated atlas pages; STL imports are triangle meshes with optional binary Magics face colors, but no standard units, textures, UVs, or hierarchy. ## Live demo: UV-textured OBJ The broken stone slab below uses `stone.obj` with a companion `stone.mtl` file that points at `stone_low_Material_Diffuse.png`. Each UV-mapped face is a DOM sprite whose background is a generated atlas page. ## Declarative loading: the mesh element The simplest way to render a mesh is the mesh element with a `src`. It fetches the file, parses it, and mounts one internal render leaf per visible polygon inside a `.polycss-mesh` wrapper automatically. ```html ``` ```tsx import { PolyCamera, PolyScene, PolyMesh } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ```vue ``` ## Supported formats | Format | Extension | Notes | |--------|-----------|-------| | OBJ + MTL | `.obj` + `.mtl` | Text format. UV maps via `vt`. Material textures from `map_Kd`. | | STL | `.stl` | ASCII or binary triangle mesh. Supports binary Magics face colors; STL has no standard units, textures, UVs, or hierarchy. | | glTF | `.gltf` | JSON format. Embedded or external buffers. TEXCOORD_0 UVs. | | GLB | `.glb` | Binary glTF. Embedded textures extracted as blob URLs. | | MagicaVoxel | `.vox` | Voxel format. Exposed faces become colored polygon quads; eligible vanilla, React, and Vue meshes use a baked direct-voxel fast path. | ## OBJ with MTL When your OBJ has a companion MTL file with textures, PolyCSS reads `map_Kd` entries and applies them as UV-mapped textures. Pass the `mtl` attribute / prop: ```html ``` ```tsx // React ``` ## Material overrides Override material colors or textures without modifying the source files (React / Vue prop form): ```tsx ``` For glTF/GLB files that preserved UVs but lost an external image reference, use the same material-name texture override under `gltfOptions`: ```tsx ``` ## Imperative loading For programmatic loading with explicit lifecycle, use `loadMesh` from the core parser. This is the universal vanilla path; React adds a `usePolyMesh` hook on top: ```ts // Vanilla: works anywhere, no framework import { createPolyCamera, loadMesh, createPolyScene } from "@layoutit/polycss"; const result = await loadMesh("https://polycss.com/gallery/obj/cottage.obj", { mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl", gltfOptions: { targetSize: 60 }, }); const camera = createPolyCamera({ rotX: 65, rotY: 45 }); const scene = createPolyScene(document.getElementById("host")!, { camera }); scene.add(result); // later: scene.destroy(); // removes the scene and disposes registered meshes ``` ```tsx // React: usePolyMesh wraps loadMesh + dispose() on unmount import { PolyCamera, PolyScene, Poly, usePolyMesh } from "@layoutit/polycss-react"; function Viewer() { const { polygons, loading, error } = usePolyMesh("https://polycss.com/gallery/obj/cottage.obj", { mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl", }); if (loading) return
Loading...
; if (error) return
Error: {error.message}
; return ( {polygons.map((p, i) => )} ); } ``` ## Atlas pipeline For textured polygons, PolyCSS runs a one-time atlas canvas pass at mount. Flat-color polygons can bypass the atlas when they can render as CSS solids or `border-shape` polygons. 1. Extract the texture image from the file (or fetch it by URL) when the polygon has a texture. 2. Solve a 6-DOF affine transform from the polygon's UV coordinates to its 2D screen footprint when UVs are available. 3. Pack polygon footprints into one or more atlas pages. 4. Clip, draw texture pixels or shaded color fills, and export atlas pages to blob URLs via `canvas.toBlob()`. `textureQuality="auto"` can rasterize these pages below full CSS resolution when packed pages would create oversized runtime bitmaps, and also selects the atlas leaf sprite size used for CSS compositing. 5. Repair antialiased atlas pixels along shared textured edges, then render each polygon as an `` with `background-image`, `background-size`, and `background-position`. Generated atlas blob URLs are revoked on unmount (call `dispose()` or let `PolyMesh` / `usePolyMesh` handle it). ## Tips - **`targetSize`**: scale the model so its longest axis fits this many world units (default: `60`). `.vox` models snap to the nearest integer voxel CSS cell size, so the final size may differ slightly to keep voxel fast-path coordinates integral. - **`paletteMergeDistance` / `colorRegionMergeDistance`**: for `.vox` files with noisy palettes, fold nearby opaque, hue-compatible colors before greedy meshing and optionally clean up small local color islands/streaks. These are lossy and change authored colors, but can reduce material count and split-quad output. In the gallery and builder, the Mesh resolution control applies them only in `Lossy` mode; `Lossless` keeps the authored palette exact. - **`solidTextureSamples`**: when enabled through `loadMesh`, texture-backed faces whose sampled UV region is effectively one color are converted to solid-color polygons before optimization. This avoids atlas slices for assets that use texture images as color swatches. - In the gallery and builder, Mesh resolution `Lossy` also collapses nearby colors produced by solid texture sampling on OBJ/GLB assets before mesh optimization. `Lossless` keeps those sampled colors exact. - **`textureQuality`**: leave at `"auto"` for workload-based bitmap caps and browser/device sprite sizing, or set a numeric raster scale for explicit quality. `0.5` uses about one quarter of the atlas bitmap memory of `1`. - Shared textured edges are repaired automatically during atlas generation. Geometry stays unchanged; only low-alpha atlas pixels at those shared edges are filled from nearby opaque texels. - **`baseUrl`**: for OBJ/glTF files with external texture paths, pass the file's URL so relative paths resolve correctly. - For large meshes: blob URLs for embedded textures and generated atlases are revoked when `dispose()` is called. Always let PolyCSS manage this: don't hold references to blob URLs across remounts. ## Related - [PolyScene](/components/poly-scene): Scene props. - [Per-polygon Interaction](/guides/shapes): Click handlers and hover states on individual polygons. - [Performance](/guides/performance): Merge modes and DOM tuning. - [Headless API](/api/headless): `loadMesh`, `parseObj`, `parseGltf`, `parseVox`, and `parseStl` signatures. --- # Introduction import { Tabs, TabItem } from '@astrojs/starlight/components'; **PolyCSS** renders 3D meshes in the DOM. No WebGL, no canvas-as-scene: the rendered output is a tree of standard DOM elements positioned with `transform: matrix3d(...)`. Each visible polygon becomes one leaf DOM node you can inspect in DevTools, target with CSS, or attach events to. Internally, the renderer chooses the cheapest CSS strategy per polygon. Solid rectangles, stable quads, triangles, and clipped solids can render as CSS primitives; textured polygons and unsupported shapes fall back to generated atlas slices. Atlas rasterization happens once at mount, then camera, mesh, and light updates flow through CSS transforms and custom properties. ## Framework Support PolyCSS is **vanilla-first**. The default entry point is custom elements (``, ``, ``, ``, controls, helpers, and shapes) plus imperative APIs such as `createPolyCamera`, `createPolyScene`, and `createPolyOrbitControls`: no framework required. First-class bindings for **React** and **Vue** ship as separate packages on top of the same engine. Pick whatever fits your stack. ## Installation ```bash npm install @layoutit/polycss ``` ```bash npm install @layoutit/polycss-react ``` ```bash npm install @layoutit/polycss-vue ``` ### CDN (custom elements) You can also load PolyCSS directly from a CDN with no build step: ```html ``` ## A quick taste Render a 3D shape with zero JS: just custom elements: ```html ``` Or with React: ```tsx import { PolyCamera, PolyScene, PolyIcosahedron } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` Continue to **[Quickstart →](/quickstart)** for the full walkthrough with Vanilla JS, React, and Vue versions. ## Related - [Quickstart](/quickstart): Full install + first scene walkthrough. - [Core Concepts](/core-concepts): The mental model: PolyScene, PolyMesh, Poly, polygon data, pipeline. - [PolyCamera](/components/poly-camera): Camera component reference. - [PolyScene](/components/poly-scene): Scene component reference. --- # Quickstart import { Tabs, TabItem } from '@astrojs/starlight/components'; import PolyDemo from '../../components/PolyDemo.astro'; Get a 3D mesh rendering in your project in two steps. ## 1. Install the package ```bash npm install @layoutit/polycss ``` ```bash npm install @layoutit/polycss-react ``` ```bash npm install @layoutit/polycss-vue ``` ## 2. Add a scene and load a mesh The camera element (`` / `PolyCamera`) is normally the outer node: it owns the projection and orbital state. (The `` custom element can also stand alone and drive an implicit camera from its own attributes — see [PolyScene](/components/poly-scene).) `` / `PolyScene` is nested inside it and carries lighting and atlas options. The mesh element (`` / `PolyMesh`) loads OBJ, STL, glTF, GLB, or VOX files and renders their polygons. `PolyCamera` uses orthographic projection by default; use `PolyPerspectiveCamera` for depth foreshortening. ```html ``` ```tsx import { PolyCamera, PolyScene, PolyBox } from "@layoutit/polycss-react"; export function App() { return ( ); } ``` ```vue ``` ## Live preview ## What you get Every visible polygon in the loaded mesh becomes a real DOM element positioned with `transform: matrix3d(...)`. The renderer chooses an internal leaf strategy for each face: CSS solids for cheap rectangles/quads/triangles when possible, and atlas slices for textured or irregular faces. You can: - Inspect individual polygons in DevTools. - Target them with CSS selectors. - Attach `onClick`, `onMouseEnter`, and other standard DOM event handlers. ## Related - [Core Concepts](/core-concepts): The mental model: PolyScene, PolyMesh, Poly, polygon data, pipeline. - [PolyCamera](/components/poly-camera): Camera props and usage reference. - [PolyScene](/components/poly-scene): Scene props and mesh options. - [Loading Meshes](/guides/textures): OBJ, STL, glTF, GLB, VOX, MTL loading, and UV textures. - [Gallery](/gallery): Browse mesh models for inspiration.