Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 7 additions & 1 deletion .github/scripts/bump-versions.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Bump the version of every package under `packages/*` in lockstep.
* Bump every publishable package under `packages/*` in lockstep.
*
* Usage:
* node .github/scripts/bump-versions.mjs <patch|minor|major>
Expand Down Expand Up @@ -28,8 +28,14 @@ const packages = readdirSync(packagesRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => resolve(packagesRoot, entry.name, "package.json"))
.filter(existsSync)
.filter((file) => JSON.parse(readFileSync(file, "utf8")).private !== true)
.sort();

if (packages.length === 0) {
console.error("could not find any publishable packages");
process.exit(1);
}

const bump = process.argv[2];
if (!["patch", "minor", "major"].includes(bump)) {
console.error(`usage: ${process.argv[1]} <patch|minor|major>`);
Expand Down
5 changes: 3 additions & 2 deletions .github/scripts/sync-package-readmes.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { copyFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { dirname, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
Expand All @@ -11,11 +11,12 @@ const targets = [
"packages/vue/README.md",
];
const packageSpecificTargets = [
"packages/domformat/README.md",
"packages/fonts/README.md",
"packages/morph/README.md",
];

const invokedFrom = relative(repoRoot, process.cwd());
const invokedFrom = relative(repoRoot, process.cwd()).split(sep).join("/");
const invokedFromPackageReadme = invokedFrom.startsWith("packages/")
? `${invokedFrom}/README.md`
: undefined;
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Enforce Morph coverage
run: pnpm --filter @layoutit/polycss-morph test:coverage

- name: Certify domformat package
run: pnpm --filter @layoutit/polycss-domformat pack:check

- name: Build packages (DTS + JS)
run: pnpm build:packages

Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Publish packages

# Manual-only. Bumps every package under `packages/*` in lockstep (using
# .github/scripts/bump-versions.mjs), builds them, publishes to npm, then commits
# the version change and pushes a `v<X.Y.Z>` tag back to main.
# Manual-only. Bumps every publishable package under `packages/*` in lockstep
# (using .github/scripts/bump-versions.mjs), builds the workspace, publishes the
# public packages to npm, then commits the version change and pushes a
# `v<X.Y.Z>` tag back to main. Packages marked `private` are never versioned or
# published by this workflow.
#
# Trigger from the Actions tab ("Run workflow") or:
# gh workflow run publish-packages.yml -f bump=patch
Expand Down Expand Up @@ -64,7 +66,7 @@ jobs:
run: pnpm build:packages

- name: Publish to npm
run: pnpm --filter "./packages/*" -r publish --access public --no-git-checks
run: pnpm --filter "./packages/*" --filter "!@layoutit/polycss-domformat" -r publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Expand Down
45 changes: 41 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Monorepo layout (pnpm workspaces):
| `packages/vue` | `@layoutit/polycss-vue` | Vue 3 mirror of the React package. Owns its own copy of atlas rasterisation. Depends on `core` only. |
| `packages/fonts` | `@layoutit/polycss-fonts` | Fonts + text → extruded 3D `Polygon[]`. Hand-written TrueType (`glyf`) reader + extruder (flat/round/bevel profiles) + Google Fonts loader. Framework-agnostic (returns `Polygon[]`, no React/Vue mirror needed). Depends on `core` + `earcut`. |
| `packages/morph` | `@layoutit/polycss-morph` | Framework-agnostic prepared-model contracts, deterministic Node preparation, browser loading, retained DOM mounting, sparse deformation, controls, springs, animation, joint skinning, and prepared playback. The browser entry uses public `@layoutit/polycss` APIs; Node-only preparation lives at `@layoutit/polycss-morph/prepare`. No React/Vue mirrors. |
| `packages/domformat` | `@layoutit/polycss-domformat` | Private source-only `domformat@0` writer, reader, validator, CLI, and browser mount with repository-side conformance. Owns the producer-neutral wire contract; producer lowering stays in producer packages. Runtime installs exclude certification material. Not published. |
| `website` | `@layoutit/polycss-website` | Astro + Starlight docs site. Not published. |
| `examples/{html,vanilla,react,vue,fontcss}` | private | Per-framework Vite apps demonstrating the minimal usage for each renderer (`fontcss` demos `@layoutit/polycss-fonts`). Workspace members so they resolve to local `workspace:^` packages. Not published. |

Expand Down Expand Up @@ -93,7 +94,9 @@ All solid and atlas-backed tags work in both modes. Direct image `<s>` leaves ar

This is the load-bearing constraint behind the whole engine. **JavaScript should not run per-frame to paint polygons when the motion can be expressed as a scene, mesh, camera, or light update.** Once the scene is built and the atlas is rasterised, the browser drives most rendering through CSS — `matrix3d` transforms, `calc()`-driven custom properties, `background-blend-mode`, `border-shape`, etc.

The current exception is imported skeletal animation. glTF/GLB skinning changes each polygon independently, so the vanilla stable-DOM animation path samples the active clip in JS, keeps the leaf set mounted, caches baked stable-triangle transform frames, and pins each mounted triangle's baked color while transforms animate. Recomputing Lambert from every deformed low-poly face normal creates visible color pumping, so color refresh is internal opt-in rather than the default animation behavior. On WebKit/Safari, where stable CSS triangles fall through to solid atlas `<s>` leaves, same-topology animation updates keep the existing atlas elements and bitmap URLs mounted, cache transform frames once warmed, and hide briefly degenerate atlas triangles only until the next valid frame. That optimized path is the default; do not add a user-facing "baseline vs optimized" toggle or maintain a legacy slow path in product UI.
The renderer exception is imported skeletal animation. glTF/GLB skinning changes each polygon independently, so the vanilla stable-DOM animation path samples the active clip in JS, keeps the leaf set mounted, caches baked stable-triangle transform frames, and pins each mounted triangle's baked color while transforms animate. Recomputing Lambert from every deformed low-poly face normal creates visible color pumping, so color refresh is internal opt-in rather than the default animation behavior. On WebKit/Safari, where stable CSS triangles fall through to solid atlas `<s>` leaves, same-topology animation updates keep the existing atlas elements and bitmap URLs mounted, cache transform frames once warmed, and hide briefly degenerate atlas triangles only until the next valid frame. That optimized path is the default; do not add a user-facing "baseline vs optimized" toggle or maintain a legacy slow path in product UI.

The domformat reference mount has one separate, closed exception: it may schedule its validated fixed-rate prepared playback and interaction tables. That scheduler may write only declared sinks on the retained targets, never reconstruct topology or evaluate producer code, expressions, renderer internals, or network resources, and is disabled by `animate: false`. This is a reference implementation of an already-lowered wire profile, not a PolyCSS renderer loop.

| Where JS runs | Where JS does NOT run |
|---|---|
Expand All @@ -102,9 +105,9 @@ The current exception is imported skeletal animation. glTF/GLB skinning changes
| Atlas planning + rasterisation (one-shot to `<canvas>`, then `toBlob`) | Per-frame atlas redraw (only on baked-mode light changes) |
| Control input handling (`PolyOrbitControls`, `PolyMapControls`, `PolyTransformControls`) | Per-frame transform recomputation of every polygon for camera/mesh motion — only the scene-root or mesh-root transform changes |
| Camera math (matrix4 product → scene-root `transform` CSS var) | Per-polygon JS in any hot path |
| Hover/selection raycasting (only on pointer events, not per frame) | Continuous re-rendering "ticks" |
| Hover/selection raycasting (only on pointer events, not per frame) | Continuous renderer re-rendering "ticks" |

If you find yourself wanting a `requestAnimationFrame` loop to update many DOM nodes outside skeletal animation, stop. Find the CSS variable that should be carrying the change, and update that single variable on a single ancestor. Cascading + `@property`-registered custom properties do the rest.
If you find yourself wanting a `requestAnimationFrame` loop to update many renderer DOM nodes outside skeletal animation or the closed domformat prepared-runtime exception, stop. Find the CSS variable that should be carrying the change, and update that single variable on a single ancestor. Cascading + `@property`-registered custom properties do the rest.

### PolyCSS Morph boundary

Expand Down Expand Up @@ -156,10 +159,44 @@ React or Vue wrappers.
- Product-specific source cadence, schemas, preparation provenance, mounting
paths, product behavior, and oracle evidence stay in the consuming product.

### domformat boundary

`@layoutit/polycss-domformat` is the private, producer-neutral reference package
for the experimental `domformat@0` wire contract. It is not a serialization
alias for Morph packages and does not depend on Morph or renderer internals.

- Node exposes only `buildDom`, `readDom`, `readDomFile`, `validateDocument`,
and `DomFormatError`; the CLI exposes only `encode`, `decode`, `inspect`, and
`validate`; the browser subpath exposes only `readDomBrowser`,
`readDomBrowserUrl`, and `mountDom`.
- Producers emit the closed writer manifest natively. Source parsing,
preparation, lowering, and product adapters remain in producer packages.
- The only physical form is canonical `.json` plus digest-bound external
sibling resource files. There is no `.dom` packet, gzip transport, embedded
payload, archive, or alternate packaging mode.
- Mounting follows `validate → construct → bind → initialize → publish →
destroy`, with rollback on partial failure and idempotent teardown.
- The package is source-only, `private`, and MIT-licensed. Workspace test/build
commands include it; public version-bump and npm-publish automation must not.
- Install tarballs contain only package metadata, README, CLI, and runtime
source. Specifications, conformance implementations, fixtures, viewers,
scripts, and tests remain repository-side certification material.
- The first concrete producer is website-owned:
`website/scripts/generate-gallery-domformat.mjs` lowers all Gallery presets
through shared Gallery preset/loader/presentation/animation behavior into
canonical documents under
`website/public/gallery/domformat/`. The generated 304-model corpus and its
digest-bound CSS/image siblings are website assets, never package payload or
runtime code. Its catalog pins the exact Chromium strategy environment and
per-model strategy counts; the corpus does not claim browser-neutral leaf
topology. Static documents are presentation-only; animated documents add
one Gallery-selected preferred clip at 30 Hz. Adding this producer does not
move source parsing or renderer internals into domformat.

## Naming (three.js parity)

- Brand text is **PolyCSS**. Keep lowercase `polycss` only for literal package names, import paths, CSS classes, domains, and other code identifiers.
- Every public export gets a `Poly` prefix. Exceptions are generic math types (`Vec2`, `Vec3`, `Polygon`, `PolyMaterial`) and the explicit `*/three` compatibility subpaths, where Three-compatible names are the point of the API. React/Vue components in those subpaths still use the `PolyThree` prefix.
- Every public export gets a `Poly` prefix. Exceptions are generic math types (`Vec2`, `Vec3`, `Polygon`, `PolyMaterial`), the closed versioned domformat API listed above, and the explicit `*/three` compatibility subpaths, where Three-compatible names are the point of the API. React/Vue components in those subpaths still use the `PolyThree` prefix.
- **Hooks/composables:** `usePolyCamera`, `usePolyMesh`, `usePolySceneContext`, `usePolySelect`, `usePolySelectionApi`, `usePolyAnimation`.
- **Components:** `PolyPerspectiveCamera`, `PolyOrthographicCamera`, `PolyOrbitControls`, `PolyMapControls`, `PolyTransformControls`, `PolySelect`, `PolyAxesHelper`, `PolyDirectionalLightHelper`, `PolyIframe`, `PolyThreePerspectiveCamera`, `PolyThreeOrthographicCamera`, `PolyThreeMesh`.
- **Types:** `PolyDirectionalLight`, `PolyPointLight`, `PolyAmbientLight`, `PolyTextureLightingMode`, `PolyTextureLeafSizing`, `PolyTextureBackend`, `PolyTextureImageRendering`, `PolyTextureImageLighting`, `PolyTextureProjection`, `PolyTexturePresentation`, `PolyTextureImageSource`, `PolyCameraProjection`, `PolyCameraSnapshot`, `PolyCameraSnapshotStats`, `PolyMeshTransformInput`, `PolySceneTransformInput`, `PolyAnimationMixer`, `PolyRenderStats`.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,24 @@ Each visible polygon is emitted as one leaf element; the renderer chooses the le
| `@layoutit/polycss-react` | React components, hooks, controls, and core re-exports. |
| `@layoutit/polycss-vue` | Vue 3 components, composables, controls, and core re-exports. |
| `@layoutit/polycss-morph` | Prepared-model loading, retained DOM animation, morph targets, skinning, and playback. |
| `@layoutit/polycss-domformat` | Private MIT-licensed producer-neutral `domformat@0` runtime for canonical JSON plus digest-bound sibling resources; conformance and specifications stay repository-side. Not published. |

The website-owned producer also carries a deterministic canonical JSON snapshot
of every Gallery model at `website/public/gallery/domformat/`, with digest-bound
CSS and image siblings. Its catalog pins the 640×640 Playwright Chromium
strategy environment, including engine version, device scale, media queries,
CSS feature branches, and per-model leaf-strategy counts; it does not claim
cross-engine strategy topology. Static models are presentation-only. Animated
models add the Gallery-selected preferred clip sampled at a fixed 30 Hz. The
corpus is a website asset, not package payload.
Regenerate it with `pnpm gallery:domformat`, verify exact Gallery inventory and
sibling-resource closure with `pnpm gallery:domformat:verify`, and run an
independent byte-for-byte regeneration check with
`pnpm gallery:domformat:verify:fresh`. Produce an
external direct-versus-canonical animated proof with
`pnpm gallery:domformat:prove --output /absolute/path`; it requires exact
retained DOM and computed paint semantics and reports bounded subpixel
Chromium compositor differences.

## Made with PolyCSS

Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@
"test": "pnpm --filter './packages/*' -r --if-present test",
"test:coverage": "pnpm --filter './packages/*' -r --if-present test:coverage",
"sync:readmes": "node .github/scripts/sync-package-readmes.mjs",
"publish:all": "pnpm sync:readmes && pnpm --filter './packages/*' -r publish --access public",
"publish:all": "pnpm sync:readmes && pnpm --filter './packages/*' --filter '!@layoutit/polycss-domformat' -r publish --access public",
"dev:website": "pnpm --filter @layoutit/polycss-website dev",
"gallery:domformat": "pnpm --filter @layoutit/polycss-website generate:domformat",
"gallery:domformat:prove": "pnpm --filter @layoutit/polycss-website prove:domformat",
"gallery:domformat:verify": "pnpm --filter @layoutit/polycss-website verify:domformat",
"gallery:domformat:verify:fresh": "pnpm --filter @layoutit/polycss-website verify:domformat:fresh",
"build:website": "pnpm --filter @layoutit/polycss-website build",
"bench:build": "node bench/build.mjs",
"bench:serve": "node bench/perf-serve.mjs --port 4400",
Expand Down
21 changes: 21 additions & 0 deletions packages/domformat/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Layoutit

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading