diff --git a/website/src/content/docs/api/headless.mdx b/website/src/content/docs/api/headless.mdx index 7c050008..208cd3d2 100644 --- a/website/src/content/docs/api/headless.mdx +++ b/website/src/content/docs/api/headless.mdx @@ -47,6 +47,24 @@ dispose(); // always call on cleanup --- +## `parseVox(buffer, options?)` + +Parses a MagicaVoxel `.vox` file into greedy colored polygon quads. The result also carries `voxelSource` metadata used by the vanilla baked-mode voxel fast path when the mesh remains eligible. + +```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). + +--- + ## `parseMtl(text)` Parses an MTL file text string into a material map. @@ -147,7 +165,7 @@ const cam = createIsometricCamera({ 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 `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"; @@ -218,6 +236,72 @@ Anything that satisfies that subset works, so layered helpers can compose multip --- +## `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 leaves, surface leaf categories, and bucket wrappers; it does not observe changes or mutate the scene. @@ -240,7 +324,7 @@ console.log(stats.mountedPolygonLeafCount, stats.surfaceLeafCounts); ## Custom elements (vanilla) -Register ``, ``, ``, ``, ``, and `` custom elements by importing the side-effect entry point: +Register the custom elements by importing the side-effect entry point: ```html @@ -255,14 +339,22 @@ Register ``, ``, ``, ``, ``, ``, ``. +- Scene and geometry: ``, ``, ``. +- Controls: ``, ``, ``, ``, ``. +- Helpers: ``, ``. +- Shapes: ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``. + --- ## Package Exports | Import path | Contents | |---|---| -| `@layoutit/polycss-react` | React components: `PolyScene`, `PolyMesh`, `Poly`, `PolyCamera`, hooks, render diagnostics | -| `@layoutit/polycss-vue` | Vue components: `PolyScene`, `PolyMesh`, `Poly`, `PolyCamera`, render diagnostics | -| `@layoutit/polycss` | Vanilla imperative API + custom element classes + render diagnostics | +| `@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`, `loadMesh`, types | diff --git a/website/src/content/docs/api/types.mdx b/website/src/content/docs/api/types.mdx index 8157fff4..1093927c 100644 --- a/website/src/content/docs/api/types.mdx +++ b/website/src/content/docs/api/types.mdx @@ -7,7 +7,7 @@ Core parser and math types are exported from `@layoutit/polycss-react`, `@layout ## `Polygon` -The atomic renderable primitive. Each polygon becomes one atlas-backed `` DOM element for textured and flat-color faces. +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 { @@ -80,7 +80,7 @@ interface PolyAmbientLight { ## `PolyTextureLightingMode` -Controls how texture lighting is applied to atlas-backed polygons. +Controls whether polygon lighting is baked into generated paint output or evaluated through CSS variables at runtime. ```ts type PolyTextureLightingMode = "baked" | "dynamic"; @@ -88,6 +88,18 @@ type PolyTextureLightingMode = "baked" | "dynamic"; --- +## `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`. @@ -161,6 +173,8 @@ Unified return shape from all mesh parsers (`parseObj`, `parseGltf`, `parseVox`, 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[]; @@ -177,12 +191,102 @@ interface ParseResult { materials?: string[]; animations?: ParseAnimationClip[]; sourceBytes?: number; + voxelCount?: 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; + gridShift: 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; +} + +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. @@ -199,6 +303,8 @@ interface LoadMeshOptions { gltfOptions?: GltfParseOptions; /** Forwarded to parseVox. */ voxOptions?: VoxParseOptions; + /** Convert uniform texture-backed faces into solid-color polygons before optimization. */ + solidTextureSamples?: boolean | SolidTextureSampleOptions; /** Shared mesh-resolution optimizer. Defaults to "lossy", including bounded seam repair. */ meshResolution?: "lossless" | "lossy"; } @@ -206,6 +312,23 @@ interface LoadMeshOptions { --- +## `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`. @@ -229,7 +352,7 @@ interface ObjParseOptions { targetSize?: number; /** Shift all vertices by this amount after scaling. */ gridShift?: number; - /** Fallback color for un-colored faces (default: "#cccccc"). */ + /** Fallback color for un-colored faces (default: "#888888"). */ defaultColor?: string; /** Override per-material colors (material name → CSS color). */ materialColors?: Record; @@ -256,6 +379,10 @@ interface VoxParseOptions { targetSize?: number; /** Shift all vertices by this amount after scaling. Default: 0. */ gridShift?: 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; } ``` @@ -271,7 +398,7 @@ interface GltfParseOptions { targetSize?: number; /** Shift all vertices by this amount after scaling. */ gridShift?: number; - /** Fallback color for un-colored faces. */ + /** Fallback color for un-colored faces (default: "#888888"). */ defaultColor?: string; /** Override per-material colors. */ materialColors?: Record; diff --git a/website/src/content/docs/components/poly-controls.mdx b/website/src/content/docs/components/poly-controls.mdx index 9c3d9bc0..9588ac4f 100644 --- a/website/src/content/docs/components/poly-controls.mdx +++ b/website/src/content/docs/components/poly-controls.mdx @@ -1,20 +1,22 @@ --- -title: PolyOrbitControls / PolyMapControls -description: "Additive camera input: pointer drag, wheel zoom, and frame-rate-independent autorotate." +title: Controls +description: "Camera and object controls: orbit, map, first-person, and transform gizmos." --- import { Tabs, TabItem } from '@astrojs/starlight/components'; -polycss ships two controls components that follow the three.js split: +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. -Both are modelled on the Three.js `OrbitControls` / `MapControls` pattern: the wrapping `` / `PolyCamera` element owns camera state across all paths. The controls component listens for pointer/wheel input and runs an optional `requestAnimationFrame` autorotate loop, then mutates that shared camera state. +Camera controls mutate the wrapping `` / `PolyCamera` state. Transform controls mutate the attached mesh handle via `setTransform`. -They are available as custom elements (`` / ``), via the imperative `createPolyOrbitControls` / `createPolyMapControls` API, and as React / Vue components. +They are available as custom elements, imperative APIs, and React / Vue components. -## Props +## Orbit / Map Props (React / Vue prop names use camelCase; the `` / `` custom elements accept the kebab-case form, e.g. `animate.speed` → `animate-speed`.) @@ -233,6 +235,78 @@ const controls = createPolyOrbitControls(scene, { 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. diff --git a/website/src/content/docs/components/poly-scene.mdx b/website/src/content/docs/components/poly-scene.mdx index 77a3e791..5f17b330 100644 --- a/website/src/content/docs/components/poly-scene.mdx +++ b/website/src/content/docs/components/poly-scene.mdx @@ -20,8 +20,12 @@ It's available as a custom element (``), via the imperative `createP | `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. | | `seamBleed` | `number \| "auto"` | `1.5` | Solid-primitive overscan for detected shared seam edges. `"auto"` fits each edge from the polygon plan; numbers clamp the CSS-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? }` | `{ color:"#000000", opacity:0.25, lift:0.05 }` | Appearance for dynamic-lighting cast shadows emitted by meshes with `castShadow`. | | `polygons` | `Polygon[]` | None | (Framework only.) Flat array of polygon objects rendered as direct children. Composes with JSX/slot children. | -| `children` | None | None | `` / `` / `` (vanilla) or `` / `` / `` (React / Vue). | +| `children` | None | None | Meshes, polygons, controls, helpers, selection wrappers, and transform controls. | **Camera state and input** are set on the wrapping camera element (`` / `PolyCamera`): `rot-x`, `rot-y`, `zoom`, `distance`. `` carries no camera attributes. Add a child `` / `` to enable drag, wheel, or autorotate: see [PolyOrbitControls](/components/poly-controls). @@ -31,16 +35,20 @@ It's available as a custom element (``), via the imperative `createP | 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`, `.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. Defaults to the scene value. | | `textureQuality` | `number \| "auto"` | Atlas bitmap budget and compositor sprite size. React / Vue only; vanilla meshes inherit the scene's `texture-quality`. | | `seamBleed` | `number \| "auto"` | Solid-primitive overscan for detected shared seam edges. Defaults to the scene value (`1.5`). `"auto"` fits each edge from the polygon plan; numbers clamp the CSS-pixel amount. React / Vue only; vanilla meshes inherit the scene's `seam-bleed`. | | `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 one `` shadow leaf per non-duplicate polygon when the scene uses `textureLighting="dynamic"`. | | `fallback` | `ReactNode` | Rendered while `src` is loading. (React / Vue only.) | | `errorFallback` | `(error: Error) => ReactNode` | Rendered if parse fails. (React / Vue only.) | | `children` | `(polygon, index) => ReactNode` | Per-polygon render prop / scoped slot. (React / Vue only.) | @@ -126,6 +134,35 @@ import { PolyPerspectiveCamera, PolyScene, PolyTorus, PolyBox } from "@layoutit/ ``` +### Dynamic Shadows + +Shadows are CSS-projected leaves. They are emitted only in dynamic lighting mode. + +```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 diff --git a/website/src/content/docs/core-concepts.mdx b/website/src/content/docs/core-concepts.mdx index 05362a12..e7be76b0 100644 --- a/website/src/content/docs/core-concepts.mdx +++ b/website/src/content/docs/core-concepts.mdx @@ -25,7 +25,7 @@ polycss exposes three composable concepts. Each one ships as a custom element (v - **Scene** (`` / `PolyScene`): the render tree root. Always nested inside a camera element. Sets up lighting and fills its parent element. - **Mesh** (`` / `PolyMesh`): loads a mesh from a URL (OBJ / glTF / GLB / VOX). Internally expands to one polygon child per face. Convenience wrapper around the parser + renderer. -- **Polygon** (`` / `Poly`): one polygon. The atomic primitive. Renders one atlas-backed `` 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". +- **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 => )`, so any rendered mesh can be inspected, styled, or handled per-polygon. @@ -70,6 +70,7 @@ 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; // Reflected as data-* DOM attributes } @@ -100,12 +101,26 @@ Scene content renders relative to the (0,0,0) origin. Most mesh files are author polycss is structured in three layers: 1. **Core** (`@layoutit/polycss-core`): Pure math and parsing. Handles OBJ / glTF / GLB / VOX parsing, UV decoding, lighting math, and polygon normalization. No DOM dependency. -2. **Triangle Renderer**: Takes parsed polygons and produces DOM elements. It runs a one-time off-DOM canvas atlas pass for both textured and flat-color polygons (UV affine transform when available → clip → drawImage or fill → atlas Blob URL → CSS background-position). -3. **Entry points**: The vanilla `polycss` package exposes custom elements (``, ``, ``, ``) plus an imperative `createPolyCamera` / `createPolyScene` API; this is the default surface and what the rest of these docs use first. Thin React (`@layoutit/polycss-react`) and Vue (`@layoutit/polycss-vue`) bindings (`PolyCamera`, `PolyScene`, `PolyMesh`, `Poly`) wrap the same renderer with framework-native reactivity, lifecycle, and prop updates. +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 `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. | +| `` | Cast shadow | Dynamic-lighting shadow leaves for meshes with `castShadow`. | + +You normally do not target these tags directly; use `Poly`, `PolyMesh`, classes, data attributes, or render stats. ## Automatic Polygon Merge -Before rendering, polycss automatically merges contiguous coplanar polygons that share the same material. This keeps DOM element counts low for flat surfaces without changing the rendered shape. +Before rendering, polycss automatically optimizes loaded meshes. `meshResolution: "lossless"` keeps exact planar candidates only; the default `"lossy"` mode can merge near-coplanar candidates within a bounded displacement budget and then spend a small repair budget on high-risk seams. 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. diff --git a/website/src/content/docs/guides/animation.mdx b/website/src/content/docs/guides/animation.mdx new file mode 100644 index 00000000..d5c1ae31 --- /dev/null +++ b/website/src/content/docs/guides/animation.mdx @@ -0,0 +1,118 @@ +--- +title: Animation +description: Playing glTF and GLB animation clips with polycss mesh handles. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +polycss can sample usable glTF / GLB animation clips into fresh polygon frames. The parser exposes `ParseResult.animation`; the animation mixer drives a mesh handle by calling `setPolygons()` as clips play. + +This is the main exception to the "no per-polygon JavaScript in the render loop" rule. Skinning changes polygon vertices independently, so animation samples in JavaScript while the renderer keeps the mounted DOM topology stable where possible. + +## React + +`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 && ( + + )} + + + ); +} +``` + +## Vanilla + +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); +} +``` + +## Notes + +- `meshResolution="lossless"` or `merge: false` keeps authored topology predictable for animated meshes. +- Solid triangle animation keeps each mounted triangle's baked color stable while vertices move, avoiding low-poly lighting flicker from rapidly changing face normals. +- `usePolyAnimation` and the core mixer expose familiar action methods: `play`, `stop`, `reset`, `fadeIn`, `fadeOut`, `crossFadeTo`, `setLoop`, `setEffectiveTimeScale`, and `setEffectiveWeight`. +- `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. diff --git a/website/src/content/docs/guides/performance.mdx b/website/src/content/docs/guides/performance.mdx index 657ce2cb..ebd5742c 100644 --- a/website/src/content/docs/guides/performance.mdx +++ b/website/src/content/docs/guides/performance.mdx @@ -5,7 +5,7 @@ description: Optimizing polycss scenes and understanding DOM-based rendering tra import PolyDemo from '../../../components/PolyDemo.astro'; -polycss renders everything as real DOM elements: every polygon of every mesh is an HTML element with CSS transforms. That gives you DevTools inspection, DOM events, and CSS styling on every polygon, but it means **performance scales with element count**. Understanding how to manage that count is key to building smooth scenes. +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 @@ -22,21 +22,38 @@ The sphere below starts at subdivision level 3 (320 triangles). Bump subdivision ## Polygon count matters -The dominant rendering cost in polycss is **style recalc + layout** (not paint or compositing). Each camera rotation triggers a CSS class change on the scene root, which re-evaluates descendant selectors. The more DOM nodes, the heavier this recalc. +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 polygon merge +## Automatic mesh optimization -polycss automatically merges contiguous coplanar polygons that share the same material (color + texture). The engine finds flat regions and combines them into larger elements before rendering. +polycss automatically optimizes loaded meshes before rendering. The default `meshResolution: "lossy"` path merges compatible polygons, can use bounded geometric approximation, and may add a small repair split budget to reduce high-risk visible seams. `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 with same-texture neighbors whose shared-edge UVs match. Lossless mode requires coplanarity; lossy mode may project near-coplanar textured neighbors into one atlas sprite within the configured displacement budget. +- 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. +- Dynamic shadows use `` leaves only for meshes with `castShadow`. + +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 vanilla baked-mode meshes 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 @@ -87,6 +104,7 @@ Use explicit numeric values when you want to override auto raster scale: `0.5` o 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. diff --git a/website/src/content/docs/guides/shapes.mdx b/website/src/content/docs/guides/shapes.mdx index 14096a39..e6afeea9 100644 --- a/website/src/content/docs/guides/shapes.mdx +++ b/website/src/content/docs/guides/shapes.mdx @@ -38,7 +38,7 @@ const polygons = boxPolygons({ ## The polygon primitive -`` (vanilla) and `` (React / Vue) render a single polygon as an atlas-backed `` for UV-textured and flat-color faces. They forward standard DOM props (`onclick`, `class`, `style`, `aria-*`, etc.). +`` (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.). @@ -265,6 +265,39 @@ const selected = ref(null); +## 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`. + ## 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. diff --git a/website/src/content/docs/guides/textures.mdx b/website/src/content/docs/guides/textures.mdx index c85e07f1..e81eda0d 100644 --- a/website/src/content/docs/guides/textures.mdx +++ b/website/src/content/docs/guides/textures.mdx @@ -6,7 +6,7 @@ description: Loading OBJ, glTF, GLB, and VOX meshes into polycss with UV texture import { Tabs, TabItem } from '@astrojs/starlight/components'; import PolyDemo from '../../../components/PolyDemo.astro'; -polycss loads 3D mesh files (OBJ, 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; each textured polygon is an atlas-backed `` that uses CSS background positioning. +polycss loads 3D mesh files (OBJ, 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; each atlas-backed polygon is an internal `` leaf that uses CSS background positioning. ## Live demo: UV-textured GLB @@ -83,7 +83,7 @@ import { PolyCamera, PolyScene, PolyMesh } from "@layoutit/polycss-vue"; | OBJ + MTL | `.obj` + `.mtl` | Text format. UV maps via `vt`. Material textures from `map_Kd`. | | 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. | +| MagicaVoxel | `.vox` | Voxel format. Exposed faces become colored polygon quads; eligible vanilla meshes use a baked direct-voxel fast path. | ## OBJ with MTL @@ -112,9 +112,9 @@ Override material colors or textures without modifying the source files (React / src="/character.obj" parseOptions={{ objOptions: { - materialColors: { Skin: "#f4c2a1" }, - materialTextures: { Body: "/body-diffuse.png" }, - includeObjects: ["Body", "Head"], // only these objects + materialColors: { Skin: "#f4c2a1" }, + materialTextures: { Body: "/body-diffuse.png" }, + includeObjects: ["Body", "Head"], // only these objects }, }} /> @@ -189,6 +189,9 @@ Generated atlas blob URLs are revoked on unmount (call `dispose()` or let `PolyM ## 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. diff --git a/website/src/content/docs/introduction.mdx b/website/src/content/docs/introduction.mdx index 1de69877..bb7b64e8 100644 --- a/website/src/content/docs/introduction.mdx +++ b/website/src/content/docs/introduction.mdx @@ -5,13 +5,13 @@ description: "polycss is a CSS polygon-mesh renderer: DOM-native 3D without WebG import { Tabs, TabItem } from '@astrojs/starlight/components'; -**polycss** renders textured 3D meshes in the DOM. No WebGL, no canvas-as-scene: the rendered output is a tree of standard DOM elements (atlas-backed `` sprites for both textured and flat-color faces) positioned with `transform: matrix3d(...)`. Each polygon is one DOM node you can target with CSS, attach event handlers to, and inspect in DevTools. +**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, polycss uses an off-DOM canvas to pack textured and flat-color polygons into atlas pages once at mount; each polygon then references its atlas region with CSS background positioning. So the user-visible runtime tree is plain DOM, with one-time atlas generation at load. +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 (``, ``, ``, ``) plus an imperative `createPolyCamera` / `createPolyScene` API: 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. +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 diff --git a/website/src/content/docs/quickstart.mdx b/website/src/content/docs/quickstart.mdx index e22124c5..894078ab 100644 --- a/website/src/content/docs/quickstart.mdx +++ b/website/src/content/docs/quickstart.mdx @@ -88,7 +88,7 @@ import { PolyCamera, PolyScene, PolyBox } from "@layoutit/polycss-vue"; ## What you get -Every polygon in the loaded mesh becomes a real DOM element: an atlas-backed `` for UV-textured and flat-color polygons, positioned with `transform: matrix3d(...)`. You can: +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.