Skip to content

Core Concepts

This page covers the mental model behind PolyCSS. Each section describes a building block and how it composes with the others.

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.

Loading…

PolyCSS exposes three composable concepts. Each one ships as a custom element (vanilla) and as a React / Vue component:

  • Scene (<poly-scene> / PolyScene): the render tree root. Always nested inside a camera element. Sets up lighting and fills its parent element.
  • Mesh (<poly-mesh> / PolyMesh): loads a mesh from a URL (OBJ / STL / glTF / GLB / VOX). Internally expands to one polygon child per face. Convenience wrapper around the parser + renderer.
  • Polygon (<poly-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 mesh element is internally polygons.map(p => <poly-polygon …>), so any rendered mesh can be inspected, styled, or handled per-polygon.

The camera element (<poly-camera> / PolyCamera) is always the outer node. <poly-scene> / PolyScene is nested inside it. Camera attributes (rot-x, rot-y, zoom, distance) belong on the camera element, never on the scene. PolyCamera is orthographic by default; use PolyPerspectiveCamera for depth foreshortening.

<!-- Vanilla -->
<poly-camera rot-x="65" rot-y="45">
<poly-scene
directional-light='{"direction":[0.5,-0.7,0.6],"color":"#ffe4a8"}'
ambient-light='{"intensity":0.4}'>
<poly-octahedron size="100" color="#7dd3fc"></poly-octahedron>
</poly-scene>
</poly-camera>
// React
<PolyCamera rotX={65} rotY={45}>
<PolyScene
directionalLight={{
direction: [0.5, -0.7, 0.6],
color: "#ffe4a8",
}}
ambientLight={{ intensity: 0.4 }}
>
<PolyOctahedron size={100} color="#7dd3fc" />
</PolyScene>
</PolyCamera>

See PolyCamera for the full prop table, defaults, and usage patterns.

Each polygon is a plain object. The only required field is vertices (three or more [x, y, z] points in world space):

interface Polygon {
vertices: [number, number, number][]; // Required: 3+ [x, y, z] points in world space
color?: string; // CSS color ("#f97316", "tomato")
texture?: string; // Image URL for UV-mapped face
material?: PolyMaterial; // Shared texture material
uvs?: [number, number][]; // UV coordinates (one per vertex)
data?: Record<string, string | number | boolean>; // 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.

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.

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:

<!-- Vanilla -->
<poly-mesh src="/model.glb" auto-center position="[0,0,0]"></poly-mesh>
// React
<PolyMesh src="/model.glb" autoCenter position={[0, 0, 0]} />

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 (<poly-camera>, <poly-scene>, <poly-mesh>, <poly-polygon>, 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.

The internal leaf tag is a strategy, not public API:

TagStrategyTypical use
<b>Solid quadAxis-aligned rectangles and stable projective quads.
<u>Stable triangle / corner-shape solidSolid triangles and exact beveled-corner solids.
<i>Border-shape clipped solidSolid non-rect polygons on browsers with border-shape.
<s>Atlas sliceTextured 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 the scene ground or receiver surfaces in both lighting modes.

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 imports use the conservative lossless path and skip ray-based interior culling in both modes because public CAD/STL files often contain shell, winding, or topology quirks. 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.