{"_id":"@anvilkit/canvas-core","_rev":"5-d007a10101bbee9be349fd5100882c04","name":"@anvilkit/canvas-core","dist-tags":{"latest":"0.1.2-rc.1"},"versions":{"0.1.0":{"name":"@anvilkit/canvas-core","version":"0.1.0","license":"MIT","_id":"@anvilkit/canvas-core@0.1.0","maintainers":[{"name":"anvilkit","email":"ancyloce@gmail.com"}],"dist":{"shasum":"5f157726f27b29e5604d2aad7d3ab4da8af0164d","tarball":"https://registry.npmjs.org/@anvilkit/canvas-core/-/canvas-core-0.1.0.tgz","fileCount":57,"integrity":"sha512-qmgVmz2JmanIuH9dKRUFWhvPwgjko+qHMLAiiD1JGL3LONxmHJJyw3VCEytn8jyjUQCo/lFjLgtaF2TBxKgEZw==","signatures":[{"sig":"MEQCIGHoDn6dxQBqv2F7cfN0QR1mQQYtb6bKMsAGWk7eRoMCAiB3Wgs2aQbCQWLzKwVBLJ2ssqCwCTWLcSFYKacR4YYDnQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":172170},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./*":{"import":{"types":"./dist/*.d.ts","default":"./dist/*.js"},"require":{"types":"./dist/*.d.cts","default":"./dist/*.cjs"}}},"scripts":{"dev":"rslib build --watch","lint":"pnpm exec biome lint package.json tsconfig.json rslib.config.ts vitest.config.ts src","size":"size-limit","test":"vitest run","build":"rslib build","format":"pnpm exec biome check --write package.json tsconfig.json rslib.config.ts vitest.config.ts src","publint":"publint","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"anvilkit","email":"ancyloce@gmail.com"},"description":"Headless Canvas IR, Zod validators, walkers, mutations, and serializers for AnvilKit Canvas Studio. No React, no Konva.","directories":{},"sideEffects":false,"_nodeVersion":"24.9.0","dependencies":{"zod":"^4.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.7","publint":"^0.3.21","typescript":"6.0.3","@rslib/core":"^0.21.5","@types/node":"^25.9.1","@anvilkit/biome-config":"0.0.1","@anvilkit/vitest-config":"0.0.1","@anvilkit/typescript-config":"0.0.1"},"_npmOperationalInternal":{"tmp":"tmp/canvas-core_0.1.0_1779354774000_0.0035769185708869333","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@anvilkit/canvas-core","version":"0.1.1","license":"MIT","_id":"@anvilkit/canvas-core@0.1.1","maintainers":[{"name":"anvilkit","email":"ancyloce@gmail.com"}],"dist":{"shasum":"316d5cfb26df8618b9eafcdd50f905e6febcae66","tarball":"https://registry.npmjs.org/@anvilkit/canvas-core/-/canvas-core-0.1.1.tgz","fileCount":69,"integrity":"sha512-oFqW4sNxUqURWRoLY7HuIKdMkJswB174zi4JrJrC6vyS4dPD6hNE0qmGvBPnyWZN0h9eCxdLDnHts0FU6B/G0A==","signatures":[{"sig":"MEUCIGD0nt/b3ImqUnpDDlHUn13OEuFMOcVFzBtTHO1YWWr8AiEAzYOr8Nyb77E2n6cP+PNQoKuGy8nECcPktvfbqNO9vOI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":271629},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./*":{"import":{"types":"./dist/*.d.ts","default":"./dist/*.js"},"require":{"types":"./dist/*.d.cts","default":"./dist/*.cjs"}}},"scripts":{"dev":"rslib build --watch","lint":"pnpm exec biome lint package.json tsconfig.json rslib.config.ts vitest.config.ts src","size":"size-limit","test":"vitest run","build":"rslib build","format":"pnpm exec biome check --write package.json tsconfig.json rslib.config.ts vitest.config.ts src","publint":"publint","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"anvilkit","email":"ancyloce@gmail.com"},"description":"Headless Canvas IR, Zod validators, walkers, mutations, and serializers for AnvilKit Canvas Studio. No React, no Konva.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.7","publint":"^0.3.21","typescript":"6.0.3","@rslib/core":"^0.21.5","@types/node":"^25.9.1","@anvilkit/biome-config":"0.0.1","@anvilkit/vitest-config":"0.0.1","@anvilkit/typescript-config":"0.0.1"},"_npmOperationalInternal":{"tmp":"tmp/canvas-core_0.1.1_1779844324447_0.3510980682747753","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@anvilkit/canvas-core","version":"0.1.2","license":"MIT","_id":"@anvilkit/canvas-core@0.1.2","maintainers":[{"name":"anvilkit","email":"ancyloce@gmail.com"}],"dist":{"shasum":"8fd8762653c72b629eb16295a21a8534c8c3cba6","tarball":"https://registry.npmjs.org/@anvilkit/canvas-core/-/canvas-core-0.1.2.tgz","fileCount":69,"integrity":"sha512-6o6nKLMsLxNkZg9kkFhynSPnWvNs2h/xrMwTqvIBR8hzfJbjV8NWlGbUvDOoXkXOnAIqyqGYJdrOszF/ejQqFg==","signatures":[{"sig":"MEUCIQD8BMpS9XgBPaRCSpZZWckec+yS/yEdmOomaLyI2wevWwIgDM20wwV5K13Ghwm8683uL3aCkGlM4Tijoo1uNRNTxMw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":271629},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./*":{"import":{"types":"./dist/*.d.ts","default":"./dist/*.js"},"require":{"types":"./dist/*.d.cts","default":"./dist/*.cjs"}}},"scripts":{"dev":"rslib build --watch","lint":"pnpm exec biome lint package.json tsconfig.json rslib.config.ts vitest.config.ts src","size":"size-limit","test":"vitest run","build":"rslib build","format":"pnpm exec biome check --write package.json tsconfig.json rslib.config.ts vitest.config.ts src","publint":"publint","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"anvilkit","email":"ancyloce@gmail.com"},"description":"Headless Canvas IR, Zod validators, walkers, mutations, and serializers for AnvilKit Canvas Studio. No React, no Konva.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.7","publint":"^0.3.21","typescript":"6.0.3","@rslib/core":"^0.21.5","@types/node":"^25.9.1","@anvilkit/biome-config":"0.0.1","@anvilkit/vitest-config":"0.0.1","@anvilkit/typescript-config":"0.0.1"},"_npmOperationalInternal":{"tmp":"tmp/canvas-core_0.1.2_1780009497204_0.7351891097803123","host":"s3://npm-registry-packages-npm-production"}},"0.1.2-rc.0":{"name":"@anvilkit/canvas-core","version":"0.1.2-rc.0","license":"MIT","_id":"@anvilkit/canvas-core@0.1.2-rc.0","maintainers":[{"name":"anvilkit","email":"ancyloce@gmail.com"}],"dist":{"shasum":"6c2b3f248d7a7f184f1b67ace260bd3458464ff2","tarball":"https://registry.npmjs.org/@anvilkit/canvas-core/-/canvas-core-0.1.2-rc.0.tgz","fileCount":135,"integrity":"sha512-zOw5dUYOH2+8ReSdMYHymtByVe0Hyvnn2ZfcHZKUxLoecGJIygLMpVZcv3IXsMc9WDSJi15waRZz4QQzIa/p5A==","signatures":[{"sig":"MEYCIQDVo9NOtvhZEHEhePQaKKCe2CK18MsDCFvouf7veuR/6QIhALGbw/b6P41OXJYpvoGbuZfpn6fY+Wrwe9UnA/Z1W+YV","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":447386},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"dev":"rslib build --watch","lint":"pnpm exec biome lint package.json tsconfig.json rslib.config.ts vitest.config.ts src","size":"size-limit","test":"vitest run","build":"rslib build","format":"pnpm exec biome check --write package.json tsconfig.json rslib.config.ts vitest.config.ts src","publint":"publint","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"anvilkit","email":"ancyloce@gmail.com"},"description":"Headless Canvas IR, Zod validators, walkers, mutations, and serializers for AnvilKit Canvas Studio. No React, no Konva.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.7","publint":"^0.3.21","typescript":"6.0.3","@rslib/core":"^0.22.0","@types/node":"^25.9.1","@anvilkit/biome-config":"0.0.1","@anvilkit/vitest-config":"0.0.1","@anvilkit/typescript-config":"0.0.1"},"_npmOperationalInternal":{"tmp":"tmp/canvas-core_0.1.2-rc.0_1781242682241_0.8923655623309505","host":"s3://npm-registry-packages-npm-production"}},"0.1.2-rc.1":{"name":"@anvilkit/canvas-core","version":"0.1.2-rc.1","description":"Headless Canvas IR, Zod validators, walkers, mutations, and serializers for AnvilKit Canvas Studio. No React, no Konva.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/cjs/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"license":"MIT","sideEffects":false,"publishConfig":{"access":"public"},"dependencies":{"pdf-lib":"^1.17.1","zod":"^4.4.3"},"devDependencies":{"@rslib/core":"^0.23.2","@types/node":"^26.1.1","esbuild":"^0.28.1","fast-check":"^4.9.0","madge":"^8.0.0","publint":"^0.3.22","typedoc":"^0.28.20","typedoc-plugin-markdown":"^4.12.0","typescript":"7.0.2","vitest":"^4.1.10","@anvilkit/biome-config":"0.0.1","@anvilkit/typescript-config":"0.0.1","@anvilkit/vitest-config":"0.0.1"},"scripts":{"build":"rslib build --lib esm && rslib build --lib cjs --no-clean","dev":"rslib build --watch","lint":"pnpm exec biome lint package.json tsconfig.json rslib.config.ts vitest.config.ts vitest.bench.config.ts src bench","format":"pnpm exec biome check --write package.json tsconfig.json rslib.config.ts vitest.config.ts vitest.bench.config.ts src bench","typecheck":"tsc --noEmit","test":"vitest run","test:coverage":"vitest run --coverage","test:watch":"vitest","bench:layout":"vitest run --config vitest.bench.config.ts","publint":"publint","check:publint":"node ./scripts/check-publint.mjs","check:circular":"madge --circular --extensions ts src/","check:layering":"node ./scripts/check-layering.mjs","check:react-free-runtime":"node ./scripts/check-react-free.mjs","check:peer-deps":"node ./scripts/check-peer-deps.mjs","check:bundle-budget":"node ./scripts/check-bundle-budget.mjs","check:api-snapshot":"node ./scripts/check-api-snapshot.mjs","check:all":"pnpm check:publint && pnpm check:circular && pnpm check:layering && pnpm check:react-free-runtime && pnpm check:peer-deps && pnpm check:bundle-budget && pnpm check:api-snapshot","verify":"pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm build && pnpm check:all","update:api-snapshot":"node ./scripts/check-api-snapshot.mjs --update","size":"size-limit"},"_nodeVersion":"22.23.1","_id":"@anvilkit/canvas-core@0.1.2-rc.1","dist":{"integrity":"sha512-eqI41+dEYD8Xxqra54YzTKN4az7vJFDDVlCTk1QXBtkn2Lf3m3Ce8xOI2or2nsTmsDRCMI2NyFAybmlARWIccw==","shasum":"0c7cc3ac10d90139d3c98c6d078364b5f9f2fdb4","tarball":"https://registry.npmjs.org/@anvilkit/canvas-core/-/canvas-core-0.1.2-rc.1.tgz","fileCount":396,"unpackedSize":1478032,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDxw4JEBNkMgnENXUiMGhuXaPkGFTmTk0CojtyFNyQQ/AiAOy77/Xf9DYPXjv0j0meHtTNolq9CZVKorWh9drOJ1pg=="}]},"_npmUser":{"name":"anvilkit","email":"ancyloce@gmail.com"},"directories":{},"maintainers":[{"name":"anvilkit","email":"ancyloce@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/canvas-core_0.1.2-rc.1_1785229971329_0.7025145264119226"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-21T09:12:53.843Z","modified":"2026-07-28T09:12:51.657Z","0.1.0":"2026-05-21T09:12:54.142Z","0.1.1":"2026-05-27T01:12:04.660Z","0.1.2":"2026-05-28T23:04:57.345Z","0.1.2-rc.0":"2026-06-12T05:38:02.404Z","0.1.2-rc.1":"2026-07-28T09:12:51.518Z"},"license":"MIT","description":"Headless Canvas IR, Zod validators, walkers, mutations, and serializers for AnvilKit Canvas Studio. No React, no Konva.","maintainers":[{"name":"anvilkit","email":"ancyloce@gmail.com"}],"readme":"# @anvilkit/canvas-core\n\nHeadless Canvas IR, Zod validators, tree walkers, immutable mutations, an\nundoable command runtime, geometry/snap math, an extension runtime, and SVG/PDF\nserializers for **AnvilKit Canvas Studio**.\n\nThis package is the React-free, Konva-free **data layer**. Its only dependencies\nare [`zod`](https://zod.dev) (validation) and [`pdf-lib`](https://pdf-lib.js.org)\n(PDF output). The visual editor (`@anvilkit/canvas-editor`) renders this IR to a\nKonva stage; this package never imports React, Konva, or `@anvilkit/core`, so the\nsame logic powers the editor, server-side export, headless tests, and\ncollaborative sync.\n\n```bash\npnpm add @anvilkit/canvas-core\n```\n\n**Status.** Pre-1.0 (`0.x`, currently shipping release-candidate versions —\nsee `package.json`'s `version`). The public API can still change between\nminor versions; breaking changes are called out in `CHANGELOG.md`.\n\n**Development model.** This package is developed inside the `anvilkit-studio`\nmonorepo as a git submodule with its own independent version line and publish\nlifecycle (see `docs/architecture/repository-structure.md`'s Submodule\nPolicy). `devDependencies` on `@anvilkit/biome-config`/`typescript-config`/\n`vitest-config` and any in-repo cross-package linkage resolve via\n`workspace:*` — a bare `git clone` of just this submodule plus `pnpm install`\ndoes **not** build or test standalone; check it out inside the parent\nworkspace for local development. Consuming it as a published npm dependency\n(`pnpm add @anvilkit/canvas-core`, above) is unaffected — that resolves a real\npublished version and needs no workspace context.\n\n## Core features\n\n- **Typed, versioned IR** — a `CanvasIR` document tree validated by Zod schemas\n  that preserve unknown keys (`looseObject`) for forward/backward compatibility.\n- **Immutable, single-pass mutations** — `insertNode` / `updateNode` /\n  `moveNode` / … rebuild only the spine to the touched node and reuse every\n  unchanged subtree by reference (structural sharing).\n- **Undoable command runtime** — `applyCommand` returns the next IR *plus* a\n  compact `inverse` command; `applyCommands` runs many as one all-or-nothing\n  transaction and derives granular change records. Opt-in `enforceLocked`\n  rejects mutations of locked nodes with a typed error.\n- **Editing-feature vocabulary (PRD 0012)** — reparent/apply-style/page\n  duplicate + resize commands, clipboard payload validation with hostile-input\n  caps, public ID remapping (`regenerateNodeIds`), stroke styling +\n  arrowheads, per-corner radii, image fit modes + one-color-matrix\n  adjustments, ordered `effects[]` (drop-shadow `spread`, blur), rich-text\n  strikethrough/auto-width/`verticalAlign`, image·svg `alt` text, and page\n  layout aids — every capability with schema, inverse, migration-free optional\n  fields, and serializer warnings.\n- **Geometry without a renderer** — affine matrices, viewport pan/zoom,\n  rotation-aware hit-testing/marquee, and Figma-style snap/align/distribute, all\n  over plain world-space numbers.\n- **Serializers** — `serializePageToSvg` (async, inlines remote images) and\n  `serializeDocumentToPdf` (raster-embed), both reporting fidelity `warnings`\n  instead of throwing.\n- **Extension runtime** — register custom node kinds, command handlers, and\n  schema migrations; `createCanvasRuntime` folds them into the validators,\n  dispatcher, and SVG output. Custom container node kinds are rejected at\n  registration (leaf kinds only today — see below).\n- **Semantic invariant validation** — `validateCanvasIRInvariants`/\n  `assertCanvasIRInvariants` check whole-document facts a Zod schema can't\n  (duplicate ids, dangling asset references, invalid page roots, excessive\n  tree depth), separate from and complementary to schema validation.\n\n## Core Architecture\n\nCanvas Core is a strict stack of layers. Each layer uses only the layers beneath\nit; nothing low-level depends on anything above it, so the IR and geometry can be\nconsumed à la carte without pulling in commands or serializers.\n\n```\n   ^ higher layers depend on lower layers (never the reverse)\n+-------------+--------------------------------------------------+\n| Serialize   | serializePageToSvg, serializeDocumentToPdf       |\n+-------------+--------------------------------------------------+\n| Runtime/Ext | createCanvasRuntime, kind/cmd/migration regs     |\n+-------------+--------------------------------------------------+\n| Commands    | applyCommand, applyCommands, commandToChange     |\n+-------------+--------------------------------------------------+\n| Mutations   | insertNode, removeNode, updateNode, moveNode     |\n+-------------+--------------------------------------------------+\n| Geometry    | toAffineMatrix, hitTest, computeSnap             |\n+-------------+--------------------------------------------------+\n| Walkers     | walk, findNode, parentOf, pageOf                 |\n+-------------+--------------------------------------------------+\n| IR + Schema | CanvasIR, CanvasNode union, CanvasIRSchema       |\n+-------------+--------------------------------------------------+\n   React-free, Konva-free, deps: zod + pdf-lib\n```\n\n**Layer responsibilities**\n\n- **IR + Schema** (`types.ts`, `ir-validators.ts`, `ir-builders.ts`) — the data\n  model: a discriminated `CanvasNode` union, the page/document tree, the Zod\n  schemas that decode untrusted input, and factory builders.\n- **Walkers** (`ir-walkers.ts`) — read-only traversal and lookup with a depth\n  guard (`MAX_TREE_DEPTH`): `walk`, `findNode`, `parentOf`, `pageOf`, type guards.\n- **Geometry** (`geometry.ts`, `viewport.ts`, `hit-test.ts`, `snap.ts`) — pure\n  math over world-space coordinates: affine transforms, world↔screen viewport\n  mapping, rotation-aware hit/marquee testing, and snap/align/distribute.\n- **Mutations** (`ir-mutations.ts`) — immutable, single-pass structural edits to\n  the IR tree (the only place the tree is rewritten).\n- **Commands** (`commands/`) — the undoable façade over mutations: a `CanvasCommand`\n  union, `applyCommand`/`applyCommands`, inverse generation, and change events.\n- **Runtime/Ext** (`extensions/`) — registries that fold custom node kinds,\n  command handlers, and migrations into the schemas + dispatcher.\n- **Serialize** (`serialize/`) — turn an IR page/document into SVG or PDF bytes,\n  honoring any extension `toSvg` hooks.\n\n**Data flow of one edit** — a host builds a command; `applyCommand` dispatches it\nthrough a single-pass mutation to a new immutable IR, and simultaneously yields an\n`inverse` (for the caller's history stack) and a change record (for listeners):\n\n```\n  UI / host\n     |  builds a CanvasCommand  (node.move, node.update, batch, ...)\n     v\n  +---------------+   dispatch    +-----------------------------+\n  | applyCommand  | ------------> | ir-mutations  (single-pass) |\n  | applyCommands |               | updateNode, insertNode, ... |\n  +-------+-------+               +--------------+--------------+\n          |                                      v\n          |                          new CanvasIR  (immutable,\n          |                          structural sharing)\n          |\n          +--> inverse command -----> caller-managed undo / redo stack\n          |\n          +--> commandToChange -----> CanvasChangeEmitter\n                                          +--> autosave / persistence\n                                          +--> collaboration sync\n                                          +--> editor re-render\n```\n\n## The Canvas IR\n\nA document is a `CanvasIR`: a version-tagged tree of pages, each with a root\ngroup whose `children` are one of 15 built-in node kinds (or a nested `group`\nitself) — `group`, `frame`, `rect`, `ellipse`, `polygon`, `star`, `line`,\n`path`, `text`, `rich-text`, `image`, `svg`, `ai-placeholder`, `video`,\n`audio`. Only `group` and `frame` are **containers** (hold `children`); every\nother kind is a leaf.\n\n```\nCanvasIR\n|-- version: \"2\"                          (v1 documents migrate on read — see below)\n|-- documentKind?: \"design\" | \"template-instance\" | \"export-variant\"\n|-- id, title\n|-- pages: CanvasPage[]                   (>= 1, enforced by schema AND by page.delete)\n|   |-- id, name?, size { width, height, unit, dpi? }, background\n|   |-- variantSource?    (campaign-resize provenance)\n|   |-- animation?        (page-level enter/exit motion metadata)\n|   `-- root: CanvasGroupNode\n|           `-- children: CanvasNode[]\n|               (group | frame | rect | ellipse | polygon | star | line | path\n|                | text | rich-text | image | svg | ai-placeholder | video | audio)\n|-- assets: Record<id, CanvasAssetRef>\n`-- metadata { createdAt, updatedAt, ownerId?, brandId? }\n```\n\nEvery node carries `transform` (translate/rotate/scale/skew), `bounds`, `zIndex`,\nand optional `opacity`/`visible`/`locked`/`blendMode`/`meta` (AI-source\nprovenance + per-node animation metadata). Props are serializable only — no\nfunctions or refs. The node union is a Zod `discriminatedUnion` on `type` for\nO(1) decoding. Fills (`CanvasFill`) and font families accept either a literal\nvalue or a `BrandTokenRef` — an unresolved pointer into an external brand kit\nthat a consumer (the SVG serializer's `resolveBrandToken` option, or the\neditor's brand kit) resolves; core never resolves one itself. `video`/`audio`\nare asset-reference-only (no inline media bytes, no playback) — see\n[Media support](#media-support-video--audio) below.\n\n## Entry points\n\n| Area | Exports |\n|------|---------|\n| **Builders** | `createCanvasIR`, `createPage`, `createGroup`, `createFrame`, `createRect`, `createEllipse`, `createPolygon`, `createStar`, `createLine`, `createPath`, `createText`, `createRichText`, `createImage`, `createSvg`, `createVideo`, `createAudio` |\n| **Walkers** | `walk`, `walkPage`, `findNode`, `parentOf`, `pageOf`, `isContainerNode`, `isGroupNode`, `isFrameNode`, `isLeafNode`, `isNodeOfKind`, `MAX_TREE_DEPTH`, `CanvasIRDepthError` |\n| **Mutations** (immutable) | `insertNode`, `removeNode`, `updateNode`, `moveNode`, `reorderChildren`, `replaceChildrenInParent`, `CanvasIRMutationError` |\n| **Commands** (undoable) | `applyCommand(ir, cmd) → { ir, inverse }`, `CanvasCommand`, `CanvasCommandError` — see the full command list below |\n| **Transactions** | `applyCommands(ir, cmds) → { ir, inverse, changes, records }` |\n| **Change events** | `commandToChange`, `commandToChangeRecord`, `replayChanges`, `createChangeEmitter`, `CanvasChange`, `CanvasChangeRecord`, `CanvasChangeEmitter` |\n| **Semantic invariants** | `validateCanvasIRInvariants(ir) → issues[]`, `assertCanvasIRInvariants`, `CanvasIRInvariantError` — whole-document checks (duplicate ids, dangling asset refs, invalid page roots, excessive depth) a Zod schema can't express; not run automatically on every command, call explicitly at a trust boundary |\n| **Geometry** | `toAffineMatrix`, `applyMatrix`, `multiplyMatrix`, `invertMatrix`, `decomposeMatrix`, `transformedBoundsExtent`, `AffineMatrix` |\n| **Viewport** | `viewportMatrix`, `worldToScreen`, `screenToWorld`, `ViewportDescriptor` |\n| **Hit-testing** | `hitTest`, `marqueeHits`, `pointInNode`, `nodeWorldAabb`, `Aabb` |\n| **Snap & align** | `computeSnap`, `alignRects`, `distributeRects`, `SnapInput`, `SnapResult`, `SmartGuide`, `DEFAULT_SNAP_THRESHOLD` |\n| **Validators** | `CanvasIRSchema`, per-node schemas, `migrateCanvasIR`, `CANVAS_IR_VERSION` (`\"2\"`) |\n| **Serializers** | `serializePageToSvg`, `serializeDocumentToPdf` |\n| **Extensions** | `createCanvasRuntime`, `createNodeKindRegistry`, `createCommandRegistry`, `createMigrationRegistry`, `CanvasExtension`, `CanvasRuntime`, `CanvasNodeKindDefinition`, `CanvasCommandHandler`, `CanvasExtensionError` |\n| **Brand** | `applyBrandColors`/etc. (FR-032) — reversible brand-kit apply transforms; `BrandTokenRef`, `resolveBrandToken` seam types |\n| **Templates** | `@anvilkit/canvas-templates`-compatible instantiation/resize helpers (`instantiateTemplate`, `resizeToVariants`, `CANVAS_SIZE_PRESETS`) |\n| **AI contracts** | `AiImageJobRequest`, `AiImageProvider`, `AiDesignJobRequest`, … (types) |\n| **Comment anchors** | `CanvasCommentAnchor` resolver types (FR-072) |\n| **Text contracts** | `CanvasTextMeasurer` port — a host-implemented text measurement contract core itself never implements |\n\nAll mutations and commands are **pure and immutable**: they return a new `CanvasIR`\nwith structural sharing (unchanged subtrees are reused by reference). Timestamps\ncome from an injectable `now?: () => string` (deterministic in tests).\n\n## Quick start\n\n```ts\nimport {\n  createCanvasIR,\n  createRect,\n  applyCommand,\n  serializePageToSvg,\n} from \"@anvilkit/canvas-core\";\n\n// 1. Build a document (one default 1080x1080 page).\nlet ir = createCanvasIR({ title: \"Hello\" });\nconst pageId = ir.pages[0].id;\n\n// 2. Mutate it through the undoable command runtime.\nconst { ir: next, inverse } = applyCommand(ir, {\n  type: \"node.create\",\n  pageId,\n  node: createRect({ bounds: { width: 200, height: 120 }, fill: \"#38bdf8\" }),\n});\nir = next;\n// `inverse` is the command that undoes this (here, a `node.delete`).\n\n// 3. Serialize a page to SVG (by index or page id).\nconst { svg, warnings } = await serializePageToSvg(ir, 0);\n```\n\n### Commands & history\n\n`applyCommand(ir, cmd)` returns the next IR plus a compact `inverse` command —\npush inverses onto a stack for undo, and re-applying an inverse yields a redo\ninverse. Supported: `node.create/delete/move/resize/reorder/rotate/update`,\n`image.replace`, `node.group/ungroup`, `page.create/delete/reorder/rename`, and\n`batch` (a composite of any of the above, itself invertible).\n\n`page.delete` refuses to remove a document's last remaining page — a\n`CanvasIR` must always have >= 1 page (also enforced by `CanvasIRSchema`) — and\nthrows a `CanvasCommandError` (code `\"invariant-violated\"`) instead of leaving\nan invalid document. This is enforced by the command itself, not only by an\neditor-level UI guard: a batch, undo/redo replay, or a host calling\n`applyCommand` directly are all protected.\n\nHistory is **caller-managed**: core hands you the inverse, but it keeps no stack\nof its own — the editor (or any host) owns the undo/redo stacks.\n\nFor multi-step edits, `applyCommands` runs a list as one reversible transaction\n(all-or-nothing) and also returns the granular change records:\n\n```ts\nimport { applyCommands } from \"@anvilkit/canvas-core\";\n\nconst { ir: next, inverse, changes } = applyCommands(ir, [\n  { type: \"node.move\", nodeId, from, to },\n  { type: \"node.update\", nodeId, kind: \"rect\", patch: { fill: \"#f43f5e\" } },\n]);\n// `inverse` is a single composite `batch`; `changes` feed autosave/collab/UI.\n```\n\n### Custom node kinds & commands\n\n`createCanvasRuntime(extensions)` folds custom node kinds, command handlers, and\nmigrations into the schemas and the command dispatcher. With no extensions the\nreturned `nodeSchema`/`irSchema` are identity-equal to the static module schemas,\nso the zero-extension runtime behaves exactly like the built-ins. The\nextension-aware schema validates every built-in field (`variantSource`,\n`animation`, etc.) identically to the static one — both are built from the\nsame shared shape constants, so registering a custom kind can never silently\nloosen validation of built-in fields.\n\n```ts\nimport { createCanvasRuntime } from \"@anvilkit/canvas-core\";\n\nconst runtime = createCanvasRuntime([myExtension]);\nconst { ir, inverse } = runtime.apply(ir, myCustomCommand); // built-ins unshadowable\nconst decoded = runtime.migrate(rawUntrustedDoc);           // migrate -> validate\nconst { svg } = await serializePageToSvg(ir, 0, { nodeKinds: runtime.nodeKinds });\n```\n\nA custom command handler is `CanvasCommandHandler<C, Inverse = C | CanvasCommand>`\n— its `apply` can return a genuinely custom `Inverse` command type (not just\nthe built-in `CanvasCommand` union) with no unsafe cast, and `runtime.apply<C>`\nmirrors that: called with no type argument it behaves exactly like\n`applyCommand`, called with an explicit `C` it types `inverse` as `C |\nCanvasCommand`. A `batch` command containing a custom sub-command dispatched\nthrough `runtime.apply` resolves that sub-command via the registry too — not\njust at the top level.\n\n**Container kinds are not extensible today.** `CanvasNodeKindDefinition.isContainer`\nexists on the type, but core's walkers/mutations only ever recurse into the\nstatic built-in containers (`group`, `frame`) — `createNodeKindRegistry`\nrejects `isContainer: true` on an extension kind outright (`CanvasExtensionError`,\ncode `\"container-kind-unsupported\"`) rather than silently accepting a\ndefinition that would never actually be walked. Model containment today by\nnesting your custom leaf kind inside a built-in `group`/`frame`.\n\n### Validation & decoding untrusted IR\n\nSchemas use `looseObject` (unknown keys are preserved, not stripped) so a\nversioned document round-trips through an older build without data loss. When\ndecoding persisted or peer-supplied IR, prefer `migrateCanvasIR(raw)` (or\n`runtime.migrate(raw)`) over a bare `CanvasIRSchema.parse` — it gates on\n`version` and is the seam for future schema migrations.\n\nMigration is deliberately **non-structural** for the legacy `shadow` field:\ndecode preserves it verbatim and read-time precedence via\n`resolveNodeEffects` (a present `effects` array — including an empty one —\nwins; otherwise `shadow` applies) keeps old documents rendering identically\nwithout an IR version bump. Nodes upgrade to `effects[]` lazily when edited.\nNever read `node.shadow` directly — always resolve through\n`resolveNodeEffects`. Rationale and guarantees: the decision record\n`docs/architecture/shadow-effects-normalization-decision.md`; contract tests:\n`src/ir/__tests__/shadow-effects-decode.test.ts` and\n`src/serialize/__tests__/effects.test.ts`.\n\n### Serializers\n\n- `serializePageToSvg(ir, pageSelector, options?)` → `{ svg, warnings }`. Async\n  (it can fetch + inline remote images). `pageSelector` is a page index or id.\n  Pass `validate: true` to reject a non-finite/malformed IR up front, and\n  `nodeKinds` (typically `runtime.nodeKinds`) to serialize extension nodes via\n  their `toSvg` hook. Emits an accessible `<title>` + `role=\"img\"`.\n- `serializeDocumentToPdf(ir, options)` → `{ pdf, warnings }`. PDF is\n  raster-embed (the caller supplies pre-rendered page rasters); page geometry is\n  taken from each `CanvasPage`.\n\nBoth report fidelity caveats (unsupported background, missing asset, blocked URI,\nunknown node kind, …) as `warnings` rather than throwing.\n\n## Notes\n\n- **Immutable & pure.** Every mutation/command returns a new `CanvasIR` with\n  structural sharing; your input is never mutated. Inject `now?: () => string`\n  for deterministic timestamps in tests.\n- **Headless.** No React, Konva, or `@anvilkit/core` imports — safe to run on a\n  server, in a worker, or in unit tests. The editor renders this IR separately.\n- **SVG is async and lossy-by-report.** It may fetch/inline remote images and\n  flags fidelity gaps as `warnings`; it never throws on a renderable-but-imperfect\n  node (enable `validate` to fail fast on malformed input instead).\n- **PDF is raster-embed.** Output is one rasterized page per `CanvasPage` — no\n  vector geometry or selectable text (the caller provides the rasters).\n- **Single entry point.** Everything is exported from the package root (`.`);\n  there are no subpath exports.\n\n## Release gates\n\n`pnpm check:all` runs the release-gate chain: `check:publint` (packed-tarball publint), `check:circular` (madge), `check:react-free-runtime` (React/Konva-free source scan), `check:peer-deps` (dependency-cone rules: zero peers, no React/Konva anywhere in the runtime cone), and `check:bundle-budget` (esbuild-based, budget and externals read from `.size-limit.json` so the two size gates cannot drift). `check:api-snapshot` (typedoc JSON diff of the public API; regenerate with `pnpm update:api-snapshot` and commit the result).\n\nGates assume a **full package build** first — run `pnpm build` before `pnpm check:all`.\n\n## License\n\nMIT\n","readmeFilename":""}