diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 94de478e..38a8d50b 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -14,6 +14,8 @@ "fixtures/", "target/", "third_party/", + // The frozen v2 workbench archive — kept verbatim (see archive/model-v2/README.md). + "archive/", // Generated Emscripten glue and binaries. "crates/grida-canvas-wasm/lib/bin/", "crates/grida-canvas-wasm/dist/", diff --git a/AGENTS.md b/AGENTS.md index f18fd41f..11593950 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,10 +25,11 @@ brew install ninja # macOS ```sh # check (each crate must pass independently) -cargo check -p grida -p grida-canvas-wasm -p grida_dev +cargo check -p grida -p grida-canvas-wasm -p grida_dev -p n0 -p n0-model # tests -cargo test -p grida # engine tests +cargo test -p grida # legacy engine tests +cargo test -p n0-model -p n0 # v2 engine tests (model is skia-free, fast) cargo test # all # lint / format (enforced) @@ -53,7 +54,8 @@ python3 bin/activate-flatc -- --rust -o crates/grida/src/io/generated format/gri | `crates/math2` · `csscascade` · `fonts` | foundations | | `crates/grida_dev` | dev CLI, benchmarks, reftest tooling | | `crates/grida_wpt` | web-platform-tests harness | -| `crates/n0` | the reserved public `n0` crate (future topology — a separate, deliberate task) | +| `crates/n0` · `n0-model` · `n0_dev` | the v2 engine family (the `anchor` model): skia-free model crate, resolve→drawlist→paint engine, winit/egui dev shell — promoted from the `model-v2-anchor` branch (gridaco/nothing#9) | +| `archive/model-v2/` | the frozen v2 workbench archive (phase papers, experiment verdicts, demo pages); paths inside the frozen papers refer to the pre-promotion layout — see its README's map | | `format/` | the FlatBuffers schema (`grida.fbs`) — **source of truth**; see `format/AGENTS.md` | | `docs/wg/` | the engine's normative working-group specs (canvas, format, research, feat-*) — same-repo | | `fixtures/` | test corpora (see the `fixtures` skill); **`fixtures/local/` is untracked** — large suites (resvg, W3C SVG 1.1, oxygen-icons, perf, refig) are downloaded per-machine | diff --git a/Cargo.lock b/Cargo.lock index b2c4f45c..c033217d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,6 +549,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -943,6 +952,15 @@ dependencies = [ "libm", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1028,6 +1046,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csscascade" version = "0.0.0" @@ -1160,6 +1188,16 @@ dependencies = [ "rayon", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1850,6 +1888,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -3210,6 +3258,47 @@ dependencies = [ [[package]] name = "n0" version = "0.0.0" +dependencies = [ + "gl", + "glutin", + "n0-model", + "raw-window-handle", + "serde_json", + "sha2", + "skia-safe", +] + +[[package]] +name = "n0-model" +version = "0.0.0" +dependencies = [ + "kurbo 0.13.1", + "num-bigint", + "num-rational", + "num-traits", + "quick-xml", + "serde", + "svgtypes", + "taffy", +] + +[[package]] +name = "n0_dev" +version = "0.0.0" +dependencies = [ + "egui", + "egui-winit", + "egui_glow", + "gl", + "glow", + "glutin", + "glutin-winit", + "n0", + "n0-model", + "raw-window-handle", + "skia-safe", + "winit", +] [[package]] name = "native-tls" @@ -4901,6 +4990,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 0e88960b..c6db6216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ members = [ "crates/csscascade", "crates/math2", "crates/n0", + "crates/n0-model", + "crates/n0_dev", ] # Inherited only by crates that opt in via `.workspace = true` (currently n0). diff --git a/README.md b/README.md index 92592887..51837632 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,11 @@ The graphics engine lives here. It migrated from the (2025→) carried over; Grida remains the service/editor monorepo and consumes the engine only through published artifacts. -The `n0` crate name stays reserved: the migrated crates keep their working -names, and the nothing-local topology (what becomes `n0`) is a separate, -deliberate task. +Two engines live side by side while the topology converges: the migrated +production engine (`crates/grida`, shipping as `@grida/canvas-wasm`) and +the v2 `n0` engine family (`crates/n0`), promoted from the +`model-v2-anchor` research branch. The v2 model program is tracked in +[gridaco/nothing#9](https://github.com/gridaco/nothing/issues/9). ## Workspace @@ -26,7 +28,9 @@ deliberate task. - [`crates/grida-canvas-wasm`](./crates/grida-canvas-wasm) — WASM bindings (`@grida/canvas-wasm`) - [`crates/math2`](./crates/math2) · [`crates/csscascade`](./crates/csscascade) · [`crates/fonts`](./crates/fonts) — foundations - [`crates/grida_dev`](./crates/grida_dev) · [`crates/grida_wpt`](./crates/grida_wpt) — dev tools, benchmarks, reftests -- [`crates/n0`](./crates/n0) — the reserved public `n0` crate +- [`crates/n0`](./crates/n0) — the `n0` engine (v2): resolve → drawlist → paint +- [`crates/n0-model`](./crates/n0-model) · [`crates/n0_dev`](./crates/n0_dev) — the skia-free `anchor` model · the v2 dev shell +- [`archive/model-v2/`](./model-v2) — the frozen v2 workbench archive (decision record) - [`format/`](./format) — the FlatBuffers schema (source of truth) - [`docs/wg/`](./docs/wg) — the engine's normative working-group specs diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 00000000..4e2c51f7 --- /dev/null +++ b/archive/README.md @@ -0,0 +1,12 @@ +# archive/ + +Frozen records of concluded programs — decision papers, experiment +verdicts, demo pages, and format drafts, kept as the historical record +after their living outcomes were promoted elsewhere. Content here is +verbatim history: it is not maintained, its internal relative paths may +describe pre-promotion layouts (each program's README carries the map), +and nothing builds from it. + +| program | record | +| ------- | ------ | +| [`model-v2/`](./model-v2/) — the v2 node-model redesign ("anchor"), promoted to the `crates/n0` family at [gridaco/nothing#5](https://github.com/gridaco/nothing/pull/5) | phase papers, experiment ledger E1–E10 with verdicts, DEC-0 decision record, demo pages, `anchor.fbs` drafts | diff --git a/archive/model-v2/.gitignore b/archive/model-v2/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/archive/model-v2/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/archive/model-v2/README.md b/archive/model-v2/README.md new file mode 100644 index 00000000..e2f8910a --- /dev/null +++ b/archive/model-v2/README.md @@ -0,0 +1,155 @@ +# model-v2 — node geometry / layout / transform model redesign + +Workbench for the fundamental redesign of the Grida node model: how a node's +geometry, position, size, rotation/transform, and layout participation are +represented — in the **Rust engine** (`crates/grida`) and the **format spec** +(`format/grida.fbs`). The proving stack has since grown source-format, engine, +and host harnesses needed to test that model end to end, including n0 XML +ingestion and explicit-time SVG animation. Those remain contained proofs; +production TS/editor, WASM, importer, renderer, and runtime migration are still +out of scope here. + +> **Archive note (2026-07-19).** This directory is the **frozen workbench +> archive** of the v2 model program, filed under the repo's +> [`archive/`](../README.md) home for concluded-program records (it lived +> at the repo root as `model-v2/` during the program). Its proving stack +> was promoted into the workspace at the landing of +> [gridaco/nothing#5](https://github.com/gridaco/nothing/pull/5): +> `a/lab` → [`crates/n0-model`](../crates/n0-model), `engine/` → +> [`crates/n0`](../crates/n0), `a/spike-canvas` → +> [`crates/n0_dev`](../crates/n0_dev). What remains here — the phase +> papers, the experiment dirs with their frozen outputs and verdicts, the +> demo pages, and the format drafts — is the decision record, kept +> verbatim apart from two repo-wide canonicalizations applied at +> landing: names (below) and the format term rename ("Grida XML" → +> "n0 XML", `.grida.xml` → `.n0.xml`; the Draft-0 root element +> `` is unchanged). File and directory names were +> canonicalized at landing (the +> workbench dir `a/` is now [`anchor/`](./anchor/); the experiment dirs +> dropped their `eN-` ledger prefixes; the candidate models dropped +> their letter slots — the ledger ids remain as the register keys in +> prose and in [`anchor/README.md`](./anchor/README.md)'s table). +> **Relative paths inside the frozen papers refer to the pre-promotion +> layout** (`a/lab`, `engine/`, `a/spike-canvas`); follow the map +> above. Tracking issue: +> [gridaco/nothing#9](https://github.com/gridaco/nothing/issues/9) +> (formerly gridaco/grida#957, transferred at the engine split). +> Implementation status and module boundaries live with the engine: +> [`crates/n0/ANIMATION.md`](../crates/n0/ANIMATION.md). + +## Run + +```sh +# the model crate (formerly a/lab) — full conformance suite +cargo test -p n0-model + +# the dev shell (formerly a/spike-canvas) — native skia window on the model +cargo run --release -p n0_dev + +# the demo pages (proof, model walkthrough, edge cases, DEC-0 fork, free editing) +python3 -m http.server 4173 --directory archive/model-v2/anchor/.preview +``` + +## Why this exists + +The current system answers the same question three different ways: + +- leaf nodes: baked `AffineTransform` + `size` +- containers: `position` enum + `rotation: f32` scalar + `layout_dimensions` +- format spec (`LayerTrait`): `layout` + `post_layout_transform` (unimplemented, self-flagged provisional) + +reconciled at runtime by a lossy, branchy resolver. This was never reconciled +because the underlying questions were never decided. This directory decides +them — problems first, then candidates, then spec. + +## Phase discipline + +| phase | artifact | status | +| ----------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1. Problems & harnesses | `problems.md`, `harnesses.md`, `study.md` | stable draft | +| 2. Candidate models | `paradigm.md`, `axes.md`, `models/*`, `finale.md`, `triage.md` | **DECIDED — `anchor`** (+5 triage amendments) | +| 3. Spec | normative doc + `grida.fbs` draft | **experiments RUN, model PROVEN** — E1–E10 complete with verdicts; **DEC-0 decided: VISUAL-ONLY rotation (the CSS framing), CSS-pure sizing** ([`anchor/dec0-visual-only.md`](./anchor/dec0-visual-only.md)); flips built (E-A14, cross-zero resize); conformance lab; native interactive spike (now [`crates/n0_dev`](../crates/n0_dev)); open calls parked in [`anchor/DECISIONS.md`](./anchor/DECISIONS.md). Remaining: fold deltas into a normative rewrite of `models/anchor.md` + WG graduation | +| 4. Runtime | `crates/grida` implementation | **proving engine BUILT and PROMOTED** — [`crates/n0`](../crates/n0) (formerly `engine/`, `anchor-engine`): the `resolve → drawlist → paint` pipeline + query/journal/replay/damage sockets, spike re-hosted onto it, gate green (shots byte-identical, replay deterministic, budgets baselined). The bounded explicit-time SVG animation checkpoint adds pure sampling, Base/Sample frame integration, exact-time rendering, a caller-owned playback clock, and a controlled native host; see [`crates/n0/ANIMATION.md`](../crates/n0/ANIMATION.md). Production migration remains separate | + +Ground rules: + +- **Problems before solutions.** When a new unclear part arises, it becomes a + catalog entry — not an inline patch to a proposal. +- **Every claim is evidence-linked** to current code, the format spec, or a + studied peer system. +- **No candidate survives without a harness run.** `harnesses.md` is the test + suite for designs. +- An earlier in-chat probe sketched one candidate (scalars-canonical + per-axis + anchors + layout-visible rotation + a transform-quarantine node). It is + deliberately **not recorded here as a decision** — it re-enters in phase 2 as + one candidate among others, subject to the harnesses. + +## Files + +- [`problems.md`](./problems.md) — the problem catalog (P1–P11): each unclear + part stated precisely, with its tension, option space, and evidence. +- [`harnesses.md`](./harnesses.md) — the constraints (H1–H10) any candidate + must pass, each with a concrete pass/fail probe, plus the tension map + between harnesses. +- [`study.md`](./study.md) — comparative study of peer systems (CSS, SVG, + Flutter, SwiftUI, Figma, tldraw): facts we reason with, not designs we copy. +- [`paradigm.md`](./paradigm.md) — phase-2 **candidate** paradigm + ("one box, one way"): nouns, laws, how each problem lands, trades declared, + falsification criteria. Not ratified. +- [`finale.md`](./finale.md) — the phase-2 finale, **decided: `anchor`**; + preserved with the pre-decision concession bill and deciding question. +- [`survey.md`](./survey.md) — the 32-question instrument itself, saved + clean (no answers, no verdict): administration rules, all questions with + options, and scoring guidance. Reusable for future re-runs or other + respondents. +- [`triage.md`](./triage.md) — the 2026-07-07 run of the survey: answers, + scoring key, verdict, the five amendments, and the one open (tilted) + fork. +- [`editor.md`](./editor.md) — the editor-experience operation catalog: + every gesture as **gesture → writes → effect → ripple** with stable + `OP-*` ids, the six operation laws (incl. the three sanctioned + state→intent bake moments), and per-op FORK marks. +- [`conformance.md`](./conformance.md) — the model-agnostic test corpus: + metamorphic laws, per-area invariants + edge registries, the executable + merge matrix, and the compatibility checklist (CSS/Figma/SVG/current + engine) with Y / N-deviation / spectrum verdicts. FORK rows are the + finale's probes. +- [`axes.md`](./axes.md) — the decision-space factoring: **Axis 1 = semantic + model** (`anchor` vs `bake` — decide first), **Axis 2 = representation & + mutation protocol** (struct vs sheet, key granularity — tunable after, + bounded by the atom rule). Re-scopes `sheet`; source of harnesses H11/H12. +- [`anchor/`](./anchor/) — **the winner's workbench**: the experiment ledger E1–E10 + (each with verdicts and lab tests), the decision register + ([`anchor/DECISIONS.md`](./anchor/DECISIONS.md)), the DEC-0 normative rules, the + ship-readiness census ([`anchor/LIMITS.md`](./anchor/LIMITS.md)), the peer-compat + matrix, the phase-4 engine layer programs with day-1 contracts + ([`anchor/ENGINE.md`](./anchor/ENGINE.md)), the Rust conformance lab (now + [`crates/n0-model`](../crates/n0-model)), and the native interactive + spike (now [`crates/n0_dev`](../crates/n0_dev)). +- [`models/`](./models/) — concrete candidate models, one file each, + harness-scored, best-faith. Files carry their working identifiers + (renamed from letter slots at landing); + the names are the working identifiers: + - [`models/anchor.md`](./models/anchor.md) — **`anchor`** (the anchored box model): + intent-canonical scalars, per-axis bindings, lens quarantine. Proposed + best fit. + - [`models/sheet.md`](./models/sheet.md) — **`sheet`** (the property sheet model): + CSS-faithful flat registry, rulebook conflicts, post-layout transforms. + - [`models/bake.md`](./models/bake.md) — **`bake`** (the materialized matrix + model): Figma-faithful matrix + state canonicalism, edit-time layout. + - [`models/wire.md`](./models/wire.md) — **`wire`** (the wired geometry model): + relational archetype — referent-general bindings, dataflow DAG, the WG + Level-4 destination. Priced and deferred; `anchor` grows into it + additively. + +## Relationship to existing docs + +- [`docs/wg/feat-layout/index.md`](../docs/wg/feat-layout/index.md) — the + anchor+flex+grid positioning vision (draft, gridaco/grida#437). It covers positioning + intent only and is **silent on rotation/transform and their layout + coupling** — that gap is a large part of this catalog. When phase 3 produces + a spec, it graduates into `docs/wg/` under WG doctrine (code-agnostic) and + this directory's evidence links stay behind as the working record. +- [`format/grida.fbs`](../format/grida.fbs) — the current archive draft; its + header rules (unset-vs-default, tables-over-structs, additive evolution) are + binding harnesses on whatever phase 3 encodes (see H9). diff --git a/archive/model-v2/anchor/.preview/edge.html b/archive/model-v2/anchor/.preview/edge.html new file mode 100644 index 00000000..698d2d5a --- /dev/null +++ b/archive/model-v2/anchor/.preview/edge.html @@ -0,0 +1,4053 @@ +anchor — rotation × layout edge cases + + +
+
+
+ model-v2 · anchor · edge cases — 2026-07-07 +
+

Rotation × layout, at the edges.

+

+ Nine scenes where the model’s laws intersect, + resolved by the actual Rust lab (every frame is engine output, not + a mockup). The question per scene: does it read naturally? The + discontinuity meter is the objective proxy — the biggest single-step + jump of any box through the whole sweep. Two scenes fail it; they are now + findings E-A11/E-A12, not footnotes. The ninth scene sweeps a drag, + not an angle — the cross-zero resize decision (DEC-9), three + policies side by side. +

+
+ +
+ +
+
+ θ = 0° + + +
+ +
+ container + biggest single-step box jump this sweep + + +
+
+
+
+ + diff --git a/archive/model-v2/anchor/.preview/fork.html b/archive/model-v2/anchor/.preview/fork.html new file mode 100644 index 00000000..2c2052e5 --- /dev/null +++ b/archive/model-v2/anchor/.preview/fork.html @@ -0,0 +1,6837 @@ +anchor — DEC-0: the framing fork, in action + + +
+
+
+ model-v2 · anchor · DEC-0 — 2026-07-07 +
+

The framing fork, in action.

+

+ The same seven scenes, the same slider, resolved twice by the same lab: + left = layout-visible rotation (anchor as locked by E1; + Figma’s framing) · right = visual-only rotation (the + CSS framing — rotation as post-layout paint transform). Every frame + is real resolver output; the meters are measured on resolved world bounds. + Left tax: the fill×rotation pops (DEC-1). Right tax: overlap, + frozen containment, and broken Figma parity. Watch the meters. +

+
+ +
+ +
+
+ θ = 0° + + +
+
+
+

+ layout-visible — anchor / Figma (E1) +

+ +
+ sibling overlap 0 px² + container 0 + ink escape 0 px +
+
+
+

visual-only — the CSS framing

+ +
+ sibling overlap 0 px² + container 0 + ink escape 0 px +
+
+
+
+
+
+ + diff --git a/archive/model-v2/anchor/.preview/free.html b/archive/model-v2/anchor/.preview/free.html new file mode 100644 index 00000000..8956352a --- /dev/null +++ b/archive/model-v2/anchor/.preview/free.html @@ -0,0 +1,911 @@ +anchor — free context: the editing experience + + +
+
+
+ model-v2 · anchor · free context — 2026-07-07 +
+

Without layout: the base case, hands on.

+

+ A live free-context document — no layout engine runs here; + resolution is bindings + one rotation scalar, mirrored from the + lab’s free-context rules. Drag bodies to move, the corner dot to + resize, the stick to rotate. Watch two things: the + IR stays SVG-shaped, and every gesture is a + counted set of scalar writes (never a matrix). Then drag the + artboard’s right edge: the green badge is + end-pinned and the amber bar is spanned — they respond + with zero writes, because position is a relation. That is what SVG + cannot say, and it is the on-ramp to layout-as-feature. +

+
+ +
+
+

canvas — free frame, five nodes

+ +
+ click = select · drag = move (2 writes) · corner dot = + resize (2) · stick = rotate (1; group = 3 center-feel) · + right-edge grip = artboard resize (0 writes) +
+
+ select a node — reads come from the resolved tier +
+
+
+

the IR — canonical print, live

+

+      

writes — per gesture, typed

+
no gestures yet
+
+
+ +
+ what this shows + Free context is not a degraded mode — it is the model’s ground + floor. + Pin Start is literally svg x/y; rotation is one scalar with + one pivot rule (box center — so rotate is a 1-field write, no x/y + compensation, and resize-after-rotate never drifts: R-4 commutes, + lab-tested). A group is a named set with a coordinate space; dragging + it is 2 writes on the group, its children untouched. What SVG has no words + for: end / center / span bindings — responsiveness without any + layout engine, which is why “svg-first, layout-as-feature” is a + gradient here, not a mode switch: free (bindings) → constrained + (end/center/span) → flex (a frame feature). The lens (general + transform) never appears in the base case — it exists only when you + import one. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
gesturewritesfields
move2x.offset, y.offset (delta-form; works under any anchor)
rotate (boxed)1rotation — center pivot needs no compensation
rotate (group, center-feel)3rotation, x, y — Figma’s trick over legible scalars
resize (corner)2–4w, h (+ x, y from the top-left corner)
resize across zero2–3|extent|, pin, flip toggle (E-A14; out-and-back = identity)
artboard resize0end/center/span children respond in the resolved tier
move a spanned axistyped errorErr(AxisOwnedBySpan) — the wall is visible, never silent
+
+
+ + diff --git a/archive/model-v2/anchor/.preview/index.html b/archive/model-v2/anchor/.preview/index.html new file mode 100644 index 00000000..bad085bb --- /dev/null +++ b/archive/model-v2/anchor/.preview/index.html @@ -0,0 +1,3876 @@ +anchor — visual proof + + +
+
+
+ model-v2 · anchor · proof run — 2026-07-07 +
+

The list makes room.

+

+ Every number and pixel on this page comes from the lab in + model-v2/anchor/ — the resolver that implements the anchor spec, + the sweeps it ran, and the agents that were tested against it. Three kinds + of proof, in order of strength: +

+ +
+
+
runtime
+
it runs, live
+

+ The E1 demo below is the resolver's actual output, scrubbable. On your + machine: + cargo test → + 56/56. +

+
+
+
image
+
it renders, exactly
+

+ SVG frames emitted by the resolver at key angles, magenta dashes = + computed world AABBs. +

+
+
+
record
+
it is written down
+

+ metrics.csv, truth.txt, prediction files, byte-fixpoint binaries + — every claim re-derivable. +

+
+
+ +
+ LAB 56/56 tests + E1 layout-visible locked + E2 byte-fixpoint + E3 LLM 22/22 ×2 + E4 10k → 5.4 ms + E5 1M transforms +
+
+ +
+
exhibit e1 — the deciding experiment, live
+

Rotate the middle card. Watch both worlds.

+

+ Same document, same resolver, one flag. Top: + anchor — the rotated card + participates by its oriented envelope, siblings make room, the hug + container breathes. Bottom: + CSS control — layout is frozen, + rotation is paint-only, and the red wash is sibling overlap that no + document field expresses. +

+ +
+
+
+
anchor — aabb participates
+ +
+
+
css control — visual-only
+ +
+
+
+ θ = 0° + + + + + +
+
+ anchor container 220.0 px + anchor overlap 0 px² + control overlap 0 px² + rotate gesture 1 write / step +
+
+ +
+
+
Container width vs θ
+
+ anchor breathes 220→276.6 px, peak at + θ*=atan(h/w)≈59°; control flat +
+ +
+
+
Sibling overlap area vs θ
+
+ anchor: zero at every angle · control: up to 1,830 px² +
+ +
+
+
Third card position vs θ
+
+ smooth — max step 3.45 px per 2°, inside the + √(w²+h²)·Δθ = 4.07 px bound +
+ +
+
+
Verdict
+
+ conformance R-3 and editor OP-ROT-2: POL → INV +
+

+ Locked layout-visible. Zero overlap by construction and by + measurement; displacement continuous through + 0°/90°/180°; rotation stays a one-field write. + Breathing during a full spin (56.6 px) is a hug-container phenomenon + and is exactly why motion rotation targets a lens channel — the + two-lane rule, now measured rather than argued. +

+
+
+ +
+
+ + + + anchor: AABB participates — θ=0° + + + + + + + + + + + + + + control: visual-only (CSS) — θ=0° + + + + + + + + + + + + +
θ = 0° — resolver SVG output
+
+
+ + + + anchor: AABB participates — θ=45° + + + + + + + + + + + + + + control: visual-only (CSS) — θ=45° + + + + + + + + + + + + +
+ θ = 45° — envelope 113.1×113.1 +
+
+
+ + + + anchor: AABB participates — θ=90° + + + + + + + + + + + + + + control: visual-only (CSS) — θ=90° + + + + + + + + + + + + +
+ θ = 90° — slot is exactly 100×60 +
+
+
+
+ +
+
exhibit e3 — can a model predict it cold?
+

Two fresh agents, a 150-line grammar, 22/22 exact.

+

+ Three agents were handed only the grammar and six documents — no + tools, no resolver — and asked for every resolved box. The hardest + probe is the E1 rule itself (p5, below). Frontier agents matched the + resolver on all 22 values within 0.5 px. +

+ +
+
<!-- probe p5 — rotated card in a hug row -->
+<frame name="list" w="auto" h="140"
+       layout="flex" gap="10" padding="10"
+       cross="center">
+  <shape name="a" kind="rect" w="60" h="100"/>
+  <shape name="b" kind="rect" w="60" h="100"
+         rotation="90"/>
+  <shape name="c" kind="rect" w="60" h="100"/>
+</frame>
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
p5 valueresolverfable-afable-bhaiku-c
list (hug w)260260260260
b box x,y100, 20100, 20100, 2050, 40
b world AABB80, 40, 100×60exactexactx=30
c (pushed) x190190190190
all 6 probes22 values22/2222/2217/22
+
+
+

+ Even the small-model control applied the rotation-in-flow + rule correctly — its misses were an off-by-one character + count and placing a box at slot-start instead of slot-center. Slips, not + structural confusion. Records: + text-ir/truth.txt · predictions/* +

+
+ +
+
exhibit e2 — the file format
+

Encode → decode → encode: byte-identical.

+

+ The quartet document through flatc 25.12: JSON → binary → JSON + → binary produces the same bytes. And "unset" is structural — + read the decoded output: +

+
+
// grow-text node — decoded JSON
+{
+  "id": 5, // no x, no y keys at all
+  "grow": 1.0  // ⇒ Pin{Start,0} by rule
+}
+// flex frame — height IS auto, structurally
+{
+  "width_type": "SizeFixed",
+  "width": { "value": 400.0 },
+  "height_type": "SizeAuto",
+  "height": { }
+}
+
+
+$ ./run.sh
+S-1 FIXPOINT: byte-identical
+M-4 forward:  v1 binary decodes under v2 schema
+M-4 backward: v2 binary decodes under v1 schema
+ max_lines: 0 ≡ unset — now unrepresentable +

+ One policy owed for phase 3: re-encoding through an old schema + drops unknown fields — read-skip is free, read-modify-write + preservation is not. +

+
+
+
+ +
+
exhibit e4 — the resolver, timed
+

10,000 nodes in 5.4 ms, unoptimized.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
scenenodesfull resolvethroughput
flat canvas (⅐ rotated)10,0015.42 ms1,844 nodes/ms
flex cards — nested hug + text measure (worst case)5,78524.4 ms237 nodes/ms
mixed groups + rotated-in-flow10,0038.21 ms1,218 nodes/ms
one card subtree (locality bound per leaf edit)618 µs
+
+

+ Single-threaded, zero caching, and the lab double-runs layout on hug + containers — this is the floor. Scaling is linear (10× nodes + → 9.6× time in the all-flex worst case). What it replaces: the + 26-arm branch forest, the atan2 transform reconstruction, and + MIN_SIZE_DIRTY_HACK. +

+
+ +
+
exhibit e5 — a million transforms
+

The corpus re-ranked the escape hatch.

+

+ 7,138 SVGs, 1,003,787 transforms scanned. The lens was designed for skew + — but genuine shear is under 1%. The real finding is + flip at 26%: single-axis mirrors must be native header bits, or the + "quarantine" fires on a quarter of wild files. +

+ +
+ translate + 9.1% + rotate 1.3% + scale= 19.8% + scale≠ + 42.8% + flip 26.1% → + goes native + shear 0.95% → + lens +
+
+ +
+
reproduce it
+

Runtime proof, on your machine.

+
+
from the repo root
+
open model-v2/anchor/rotation-in-flow/demo.html   # the offline twin of the demo above
+cd model-v2/anchor/lab
+cargo test                        # 56/56 conformance-derived tests
+cargo run --bin e1                # regenerate the sweep, frames, metrics
+cargo run --bin e3 -- truth       # ground truth an agent must match
+cargo run --release --bin e4      # the timings above
+
+

+ Full narrative, amendments E-A1…E-A7, and the honest lose column: + model-v2/anchor/REPORT.md. + Nothing is committed to git — the workbench is yours to review. +

+
+
+ + diff --git a/archive/model-v2/anchor/.preview/model.html b/archive/model-v2/anchor/.preview/model.html new file mode 100644 index 00000000..312fef8f --- /dev/null +++ b/archive/model-v2/anchor/.preview/model.html @@ -0,0 +1,1017 @@ +anchor — how the model works + + +
+
+
+ model-v2 · anchor · model demo — 2026-07-07 +
+

One box. Content realizes into it.

+

+ The consolidated model in three interactive laws. A node is an + anchored box with typed content: the box comes from intent, content + is realized into the box at render, and nothing derivable is ever + stored. Everything below runs the spec’s own formulas (the same ones + the Rust lab asserts in 58 tests and LLMs predicted cold at 100%). +

+
+ +
+
demo a — the two-axis core
+

One box, four realizations.

+

+ Drag the box size. + The document writes are the same two fields every time (w, h) + — what differs per kind is how content re-realizes: parametric + shapes re-derive, vectors re-map from their reference rect, text re-wraps, + image paint re-fits. No points move, no matrix appears, no bake happens. +

+ +
+
+ + + + + + document writes per drag: + 2 fields +
+
+
+
shape · parametric
+
+ Descriptor = f(box). A star has no points — count 5 + + inner ratio 0.45, evaluated at the box. Stroke stays 2px. +
+ +
stored: kind=star points=5 inner=0.45
+
+
+
vector · mapped
+
+ Points live in a 24×24 reference rect (inset, frozen). + Render maps ×(box/ref). Stroke stays 2px — non-scaling + by construction. +
+ +
+ stored: ref=24×24 + 7 vertices (never rewritten) +
+
+
+
text · flowed
+
+ Content re-wraps at the imposed width (greedy, the spec metric). + Font size stays 14 — a style, not geometry. +
+ +
stored: content + size=14
+
+
+
image fill · fitted
+
+ Paint re-fits at the resolved box per fit mode — never + stretched with geometry. + +
+ +
stored: image ref + fit mode
+
+
+
+
+ +
+
demo b — the points law (e-a9)
+

Resize all you want. The blob stays the blob.

+

+ The table is the stored document data — vertices in the + reference rect. Resize writes w/h and the table never changes (Sketch + stores 0–1 points, Figma ships + normalizedSize ≠ size + nodes — same law). Only a vertex-editing gesture writes + points; then the reference bounds re-derive. +

+ +
+
+
+
+ + + + +
+ +
+
+
+
+ stored document data + FROZEN UNDER RESIZE +
+
+ w: 200   h: 200   ref: + 24×24 +
+ + + + + + +
vertexx (ref)y (ref)
+
+ resolved (read tier, materialized like Figma’s plugin API):
+ vertex #4 → +
+
+
+
+
+ +
+
demo c — the two scales
+

Resize is not scale. And scale is two different things.

+

+ One card (dotted image fill, 2px stroke, radius 10, label 12px), one + slider, three semantics. Watch the document under each: resize + writes two fields and keeps every style in px; K is a sanctioned + bake that multiplies the numbers; lens stores one op and touches no + value — the picture semantics (strokes and tiles magnify), + quarantined. +

+ +
+
+ +
+
+
+
resize
+ +
+
+
+
K · parameter scale (bake)
+ +
+
+
+
lens · picture scale (stored op)
+ +
+
+
+
+ Law 5. Styles are px-stable under resize (non-scaling-stroke by + construction). K bakes numbers through the subtree — Figma + rescale(), Sketch K, Grida parametric_scale, already shipping. A + retained picture-scale exists only as a lens op — the + SVG/Graphics-container semantics, opt-in, never a prerequisite. +
+
+
+
+ + diff --git a/archive/model-v2/anchor/COMPAT.md b/archive/model-v2/anchor/COMPAT.md new file mode 100644 index 00000000..9861b21b --- /dev/null +++ b/archive/model-v2/anchor/COMPAT.md @@ -0,0 +1,174 @@ +# COMPAT — the import maps (css → grida · svg → grida · figma → grida) + +2026-07-07. The three import sources the model must be able to absorb, +mapped primitive-by-primitive into the anchor vocabulary. This document +is also a **standing design harness (H13)**: any change to the model +must state its effect on these maps; a change that silently moves rows +toward `unsupported` is a regression even if every internal test stays +green. + +**Verdicts** — `native` (direct field map) · `surgery` (tree +restructuring, semantics preserved) · `trick` (non-obvious but exact +construction) · `lens` (quarantine wrap) · `paint` (lands in the paint +model, not node geometry) · `degrade` (approximate, loss declared) · +`unsupported` (flatten/rasterize/drop) · `out-of-scope`. + +Full row data (33 CSS + 31 SVG + 30 Figma rows with per-row edge notes) +was produced by the 2026-07-07 mapping run; the tables below are the +complete verdict census with the sharpest edges inlined. + +--- + +## 1. CSS → grida + +**Posture: the positioned/flex core is nearly the native tongue** — +insets literally are the Pin/Span vocabulary (`right:24` = `Pin{End,24}`, +`left+right` = `Span`) and flexbox maps 1:1 minus three declared holes. +The systematic surgeries: block flow → flex column; margins → +gap/padding/spacers; z-index → fractional-index resort. Anything scroll- +or responsiveness-coupled freezes at the import viewport **or stays live +in the htmlcss engine via the `embed` kind — the sanctioned pressure +valve** (Grida ships a real CSS engine; import-to-canvas is a choice, +not the only door). + +| primitive | verdict | map / sharpest edge | +| ------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| position:absolute + insets | **native** | Pin/Span per edge; intent survives resize. Edge: CSS containing block ≠ direct parent → re-parent surgery; auto-inset static positions bake | +| display:flex + direction/wrap | **native** | LayoutBehavior. Edge: no `*-reverse` (index-reverse surgery); no align-content; wrap-reverse unrepresentable | +| justify-content | **native** | main_align 1:1. Edge: safe/left/right collapse | +| align-items/self | **native** | cross_align/self_align. Edge: baseline = DEFER (X-CSS-9), bakes stale offsets | +| flex-grow/basis | **native** | grow + SizeIntent. Edge: `flex:1` ⇒ basis 0 ⇒ write Fixed(0)+grow:1, not Auto | +| flex-shrink | **degrade** | X-CSS-2 deviation (shrink:0); bake shrunk sizes at import viewport | +| gap | **native** | direct. % gap bakes | +| block flow / static | **surgery** | → flex column + stretch; margin collapsing must resolve _before_ conversion | +| margin | **surgery** | gap/padding/spacers; negative margins → pins or lens (visibly wrong if flow relied on displacement) | +| margin:auto | **trick** | Pin{Center}/alignment/grow-spacers | +| padding | **native** | EdgeInsets; on non-frames → wrapper | +| border + box-sizing | **paint** | strokes (border-box by construction); heterogeneous per-side borders degrade | +| position:relative | **lens** | flow slot + lens Translate — exact CSS. Edge: containing-block duty doesn't transfer | +| position:fixed | **surgery** | re-parent to viewport root; scroll immunity gone → embed for live | +| position:sticky | **degrade** | freeze at scroll 0; embed for live | +| display:grid | **degrade** | run grid once, bake cells to Pin/Span (mode:grid is the additive future); fr/minmax responsiveness lost; embed for live | +| transform translate/scale | **lens** | ordered ops, exact CSS paint-only semantics; % translate bakes | +| transform rotate (in flow) | **native** | DEC-0 flipped the default to visual-only: header rotation IS CSS rotate semantics now (lens-rotate ≡ header-rotate; X-CSS-5 fork dissolved) | +| skew/matrix | **lens** | the quarantine's founding purpose (wrap, never degrade) | +| 3D + perspective | **lens** | spec-reserved vocabulary; preserve-3d across elements doesn't compose (each lens flattens) | +| transform-origin | **trick** | Alignment enum or Translate-conjugation (costs the one-op-one-channel story) | +| overflow/clip | **native** | clips_content; scroll/auto degrade → embed for live panes | +| z-index/stacking | **surgery** | resort fractional indices; cross-hierarchy interleaving (negative z, cousins) unrepresentable | +| %-/vw/em units | **degrade** | bake to px. Exception: 100% → Span{0,0}/stretch — the one intent-preserving unit | +| min/max/fit-content | **degrade** | X-CSS-3 DEFER; text Auto covers text only; min-content floor absent | +| aspect-ratio + clamps | **native** | direct. Edge: anchor never overrides Fixed (CSS transferred-size cases diverge) | +| display:none | **native** | active:false (excluded from measure — tested) | +| display:contents | **surgery** | splice children up (group is NOT equivalent — it has a derived box) | +| inline formatting (text+atoms) | **degrade** | pure text → text kind; atom interleaving inside wrapped lines is outside the model | +| float/clear | **degrade** | bake positions; wrap-around needs exclusions we don't have | +| writing-mode / rtl | **unsupported** | X-CSS-7 declared N; BiDi is text-internal only | +| order | **surgery** | rewrite fractional indices; a11y/source order distinction erased | +| tables | **degrade** | bake column widths into nested flex; shared column sizing has no home | + +## 2. SVG → grida + +**Posture: mostly native, with the paint model and usvg pre-processing +absorbing what the node model shouldn't.** E5 already measured +transforms (native 73% / flip 26% → E-A2 / true shear 0.95% → lens). +The remaining edges cluster in four families: **viewBox proportional +scaling** (SVG's scaling-stroke default is _our_ exotic case — the exact +inversion of law 5), **instancing** (`use`/`symbol` = copy-on-import, +divergence declared), **text** (baseline math, per-glyph positioning, +textPath), and **paint servers** (`pattern` has no home yet). + +| primitive | verdict | map / sharpest edge | +| --------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| root `` viewBox + pAR | **trick** | frame Fixed×Fixed; uniform viewBox scale → K-bake at import; `pAR="none"` non-uniform **cannot** K-bake strokes → lens or accept px-stable deviation | +| absolute x/y everywhere | **native** | free frames + Start pins (current importer's model already) | +| `` | **native** | group (derived box, E-A1). Edge: `` re-kinds to frame (effects are ignored-by-rule on group) | +| `` (arcs incl.) | **native** | vector (E-A9 reference rect); arcs pre-baked to cubics by usvg (render-exact, not round-trippable) | +| `` | **native** | shape Rect + corner radius. Edge: rx≠ry elliptical corners have no home; **px-stable radius vs SVG's scaling radius under viewBox** | +| `/` | **native** | Ellipse descriptor. Edge: usvg normalizes to path — importer must pattern-match back or accept vector | +| `` | **trick** | shape/Line box construction (length × Fixed(0), rotation) | +| `/` | **native** | vector or normalized Polygon. Edge: axis-aligned lines → zero-extent ref axis (B2 POL) | +| nested `` | **surgery** | frame + clip + viewport transform (K-baked uniform / lens non-uniform) | +| `+` | **degrade** | copy-on-import (usvg); instancing divergence is the declared loss | +| `` | **surgery** | per-use instantiation; different use sizes ⇒ divergent K-bakes | +| `` / `` | **degrade** | first-passing-branch frozen / link metadata dropped | +| % units | **degrade** | baked by usvg; percent pins (a.md §12) recover this later | +| `` chunks/tspan | **surgery** | shipped model: text-import.md (group of measured text chunks); per-glyph x/y lists flatten | +| text-anchor | **trick** | is literally a Pin anchor (middle→Pin{Center}) | +| baselines | **degrade** | baseline→top conversion needs real ascent (current FIXME approximates ascent=font_size) | +| `` | **unsupported** | flatten to vector via usvg; no lens construction exists | +| markers | **surgery** | usvg bake (lossless render) or native stroke markers on open geometry | +| ``/`` | **surgery** | sibling-mask construction (importer ships it); oBB units freeze against import-time bbox | +| `` | **degrade** | blur/drop-shadow → effects; arbitrary fe-graphs unsupported (rasterize or drop) | +| gradients | **paint** | entirely in the paint model (E5 split); shared userSpace gradients denormalize per node | +| `` | **unsupported** | no Pattern paint yet (wg/feat-svg/pattern.md 'not started'); rasterize-tile fallback | +| opacity vs fill-opacity | **native** | header opacity vs paint opacity — semantically distinct, both mapped | +| paint-order | **trick** | split into stacked siblings (edit-coupling cost) | +| vector-effect: non-scaling-stroke | **native** | **the default, by construction** (law 5) — the inversion: SVG's scaling stroke is the exotic import | +| stroke props | **native** | dasharray/cap/join/miter 1:1; dashoffset currently dropped (importer gap) | +| CSS-in-SVG | **degrade** | cascade flattened at parse; @media frozen to one branch | +| SMIL | **unsupported** | static t=0 imports; animateTransform→lens-channel mapping is a future runtime story (H7) | +| `` | **degrade** | embed kind (format:html) — needs pre-usvg extraction; declared h vs measured-h mismatch | + +## 3. Figma → grida + +**Posture: the owner's "almost 100%, workaround exists" is structurally +right — Figma is the closest cousin (several anchor mechanisms are +Figma's own, measured: normalizedSize = E-A9, parametric stars, px-stable +K/rescale split, layout-visible rotation = X-FIG-1).** The honest +residue: one _model_ gap (SCALE constraint → percent pins, deferred; +prevalence unmeasured until **E9**), one _definition_ fork (bool bounds: +op-result vs Figma's operand-union — constructible divergence), and four +_importer_ gaps that are work, not model problems. + +| primitive | verdict | map / sharpest edge | +| --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| auto-layout frame | **native** | LayoutBehavior 1:1 (spacing/padding/align). Edge: flex emission is currently opt-in (`prefer_auto_layout`); default imports a position snapshot | +| primary/counter align | **native** | main/cross align (anchor superset). Edge: wrap's SPACE_BETWEEN cross-run distribution has no field; BASELINE falls to start | +| FIXED/HUG sizing | **native** | Fixed/Auto SizeIntent | +| FILL sizing | **trick** | grow:1 / self_align:Stretch / Span{0,0} — the three-spellings map. **Importer gap: not yet encoded (bakes size today)** | +| absolute-in-auto-layout | **native** | flow:Absolute + bindings | +| constraints MIN/CENTER/MAX | **native** | Pin Start/Center/End — intent stored. Edge: pins bind the unrotated box, Figma constrains the rotated AABB (§2.1's recorded v1 refinement) | +| constraint STRETCH | **native** | Span{start,end} — exactly Figma's semantics incl. size-ignored | +| constraint SCALE | **degrade** | no v1 percent pins (a.md §12 additive). Prevalence unknown → **E9** | +| **group constraint-transparency** | **surgery → native under E-A13** | children of GROUPs constrain against the _outer frame_ in Figma; anchor binds to the direct parent + E-A5 forbids non-Start under groups. Faithful import = **promote group→frame** or hoist; loses either grouping or responsiveness → **the GROUP.md fork** | +| rotation | **native** | matrix→scalar (pivot translation compensated; CCW→CW negation) | +| rotated-in-auto-layout | **degrade** | DEC-0 (visual-only): Figma makes room, anchor does not — importer bakes these to `flow:Absolute` + pins at resolved position (geometry-exact, flow participation dropped); frequency = E9 | +| flips | **native** | E-A2 flip_x/y, semantics built (E-A14: pivot per kind, `T·R·F` innermost). **Importer gap: currently bakes mirrors into path data (predates E-A2)** | +| resize across zero (drag past the fixed edge) | **native under DEC-9=flip** | Figma flips every kind (one shared render transform). Anchor `resize_drag` re-targets: extent stays \|w\|, axis flip toggles, pin re-anchors; out-and-back = document identity; typed negative W still rejects (NegativeExtent) | +| plugin-authored skew/matrix | **lens** | wrap-never-degrade; frequency unscanned (E9) | +| GROUP | **native** | derived box + origin placement; X-FIG-4 deliberately N (no re-fit writes) | +| BOOLEAN_OPERATION | **native** | op enum 1:1, operands as children. **Fork: anchor box = op-result (D-5), Figma = operand union — divergent bounds constructible** | +| vector networks | **native** | normalizedSize _is_ E-A9 (fixture-proven). Edge: non-zero blob origins need the reference RECT | +| STAR/POLYGON/ELLIPSE arc | **native** | parametric 1:1 (REST fallback loses parametricity to baked paths) | +| corner radius + smoothing | **native** | field-mapped; smoothing needs the renderer formula | +| strokes | **native** | align/dash/per-side mapped. Edge: `strokesIncludedInLayout` has no home (stroke never consumes layout) | +| effects | **native** | 4 effect types mapped. Edge: effects-on-GROUP is ignored-by-rule in anchor → re-kind to frame on import | +| fills/gradients | **paint** | per-paint transforms. Edge: flip-baked nodes keep fills in unbaked frame (importer bug class) | +| image fills | **paint** | scaleMode→BoxFit/Transform/Tile. Edge: TILE px-anchoring — re-tiles on resize (matches anchor law, differs from naive expectation) | +| text autoResize | **native** | (Auto,Auto)/(Fixed,Auto)/(Fixed,Fixed); TRUNCATE→max_lines+ellipsis. **Importer gap: maxLines parsed but never mapped** | +| masks | **surgery** | maskType 1:1 + sibling reorder (scope-start → sibling-order) | +| SECTION/SLICE | **native** | tray / dropped | +| components/instances | **out-of-scope** | geometry-lossless snapshots; override intent lossy (not this map's concern) | +| layout grids | **unsupported** | dropped; becomes visible when snap-to-grid ships | + +--- + +## Cross-cutting obligations this document creates + +1. **H13 (the harness clause).** Every future model/spec change names + its impact on these three tables. Moving a row _down_ the verdict + ladder requires the same sign-off as breaking a conformance INV. +2. **E8/E9 stay blocking** (pedantic §D): E8 = the _outbound_ CSS + projection (this doc is inbound-only by design); E9 = the .fig corpus + scan that turns "almost 100%" into a number (SCALE prevalence, skew + frequency, rotated-in-auto-layout counts). +3. **Importer work queue surfaced by the mapping run** (work, not model + flaws): Figma FILL→grow/stretch encoding; flips→flip_x/y (post-E-A2); + maxLines mapping; SVG stroke-dashoffset; group→frame re-kinding rules + (``, Figma effects-on-group, constraint-carrying group + children — pending the GROUP.md decision). +4. **The two escape valves are load-bearing**: `embed` (live web islands: + sticky/scroll/grid/media-query content) and `lens` (CSS transforms, + plugin matrices, SVG shear). Neither may regress without this + document noticing. diff --git a/archive/model-v2/anchor/DECISIONS.md b/archive/model-v2/anchor/DECISIONS.md new file mode 100644 index 00000000..05fa1dbc --- /dev/null +++ b/archive/model-v2/anchor/DECISIONS.md @@ -0,0 +1,332 @@ +# DECISIONS — the open register + +2026-07-07. Every red flag that needs an **owner call** lives here, one +entry each: the question, the evidence on file, the options with a +recommendation, and what the answer unblocks. Everything NOT here is +either already locked by evidence (REPORT amendments E-A1…E-A14) or +adopted-by-evidence below (appendix — veto window, not questions). + +Ids are `DEC-#` (the `D-#` prefix belongs to the derived-kind +conformance suite). Status: **OPEN** until the owner answers; answers +get recorded in place and promoted to amendments/spec text. + +--- + +## DEC-0 · the framing fork: is rotation layout-visible at all? — **DECIDED 2026-07-07 (second lock): VISUAL-ONLY (the CSS framing), CSS-pure sizing** + +**Owner framing, locked after an explicit correction.** The first lock +("keep layout-visible") mis-recorded the owner's sentence — "this model +is much cleaner with the behaviour" referred to the CSS framing in the +fork demo, not the anchor arm. Corrected 2026-07-07: rotation is a +**post-layout paint transform**; sizing (flex contributions, hug, +derived unions) NEVER reads rotation or flips; the read tier +(selection, world AABBs, hit-testing) stays oriented. Sub-answer: +**CSS-pure** — sizing ignores transforms everywhere, no hybrid hug. +Normative rule set + the group-box fork decision: +[`dec0-visual-only.md`](./dec0-visual-only.md). Consequences: DEC-1/2/3 +close as n/a; E-A4/E-A7/E-A11/E-A12 retire; COMPAT's Figma +rotated-in-auto-layout row degrades (importer bakes to absolute); the +lens remains the quarantine for shear/matrix (lens-rotate ≡ +header-rotate behaviorally). The flip was gated on a spec review — run +same day, findings in the rule doc. + +**Question (owner, 2026-07-07).** Adopt the CSS framing as the default — +rotation is a _post-layout paint transform_, the box never grows by +child rotation — trading make-room semantics for clean two-lane +semantics? Sub-questions answered below the table: free context, the +general transform, svg-first. + +**Evidence.** Both arms are implemented and tested (`RotationInFlow`; +R-3 runs both) — this is a policy flip, not rework. **See it in +action:** [`dec0-fork/`](./dec0-fork) — the same seven scenes resolved +under both framings side by side, live overlap/containment meters +(measured at θ=45°: CSS arm — stretch 4,131 px² overlap; hug container +frozen while ink escapes 16.6 px; wrap never reflows, 2,661 px²; anchor +arm — 0 px² everywhere except the deliberate lens lane). E1 measured the +arms against each other: anchor arm **0 px² overlap at every angle**; +CSS arm **1,830 px²** (siblings don't make room). X-FIG-1: Figma is +layout-visible — visual-only silently changes imported geometry for +every rotated child in auto-layout (frequency = **E9**'s counter, +unknown). The tax is documented on BOTH sides: Figma's forum asks to +_stop_ envelope growth (pedantic D3); CSS had to add `writing-mode` as +a whole layout feature because paint-rotation cannot put rotated text +in flow — the one rotated-in-flow case everyone needs. The lens is +already the CSS lane inside anchor +(`lens_rotate_is_the_visual_only_twin` reproduces the CSS arm exactly). + +| option | gain | lose | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **keep layout-visible (E1)** ← recommended | containment truth (0 overlap, measured); Figma import parity; rotated-text-in-flow native (demo scene reads naturally); **both lanes reachable** — the lens IS per-node CSS semantics, one structural op | the whole DEC-1/DEC-2 policy surface; E-A7 readout + D3 escapes launch-blocking; envelope math in M/L | +| flip to visual-only (CSS) | DEC-1/DEC-2/DEC-3 close n/a; fill never fights rotation (no pops, no 45° flip); simpler flex path; the D3 ticket class never happens | measured overlap by construction; Figma parity breaks (E9 quantifies); **no path back** — layout-visible becomes per-node unreachable (wrapping in a hug frame doesn't help: hug would read the unrotated box too); a NEW wart appears — hug/group must still union _oriented_ envelopes or selection chrome lies about ink, so the envelope math stays anyway and "auto size" forks meaning by context | + +**The three sub-answers (recorded so the framing question stays sharp):** + +1. _Free context is not "pre-layout" — it is the base case._ Rotation is + ALWAYS applied in phase T, in every context; the fork is only what + phases M/L **read** (oriented envelope vs unrotated box). A + scene-root rectangle rotating has no L to be pre or post of; its + envelope exists only for reads (selection, hug, union). "Pre/post + layout" is CSS vocabulary assuming flow exists everywhere — that's + the legacy framing, not the free canvas. +2. _The general transform is already settled and does not move._ The + general affine lives ONLY in the lens, and the lens is paint-only in + every context — CSS semantics already. If header rotation also went + visual-only, header-rotate and lens-rotate become redundant + everywhere (E-A8's distinction collapses; one of them turns legacy). + Layout-visible keeps them meaningfully distinct: header = the flow + lane, lens = the paint lane. +3. _svg-first + layout-as-feature is already the model's shape_ — free + context is the default world, `Pin{Start, offset}` IS svg x/y, + layout is a frame payload feature that overrides position. + **Hands-on:** [`free-editing/`](./free-editing) — a live free-context + document (no layout engine runs) with the canonical IR and a + per-gesture typed write log; the artboard-resize gesture shows + end/center/span responding with zero writes. What + svg-first must NOT become is matrix-first **storage**: E5 (99% of a + million wild transforms are structured intent), E3 (LLMs 100% on + scalars, matrices notoriously not), E4 (kills the atan2 + reconstruction forest), CRDT 1-field writes — all measured against + the matrix road. + +**The mental model, one sentence.** A scene is a transform tree with +structured intent (SVG's semantics, not SVG's storage); layout is a +container feature that computes some boxes; rotation is always applied +after the box (phase T); the ONLY open dial is whether box computation +_reads_ the rotated envelope — never (CSS) / always (Figma, anchor +today) / per-node via lens (anchor's two-lane). + +**Relation to the register.** If DEC-0 = visual-only: DEC-1/2/3 close +n/a, E-A4/E-A7/E-A11/E-A12 retire, X-FIG-1 flips to a COMPAT degrade +row. If DEC-0 = keep: the clean-semantics instinct is honored at the +actual mess instead — **DEC-1 = inert-when-rotated + DEC-2 = inert** +gives "fill never fights rotation" (most of the CSS win) while keeping +make-room, containment, and Figma parity. E9's rotated-in-auto-layout +count is the missing number either way — run it before locking. + +## DEC-1 · fill intent × rotation (E-A11 + E-A12) — **CLOSED n/a (DEC-0, 2026-07-07)** + +_Closed by the visual-only lock: the configuration this decision +governed no longer exists — sizing never reads rotation, so fill +never fights it (dec0-visual-only.md V-2/V-4). Body kept as the +record of the road not taken._ + +**Question.** What do `grow` (main axis) and `self_align:Stretch` (cross +axis) do on a **rotated** in-flow child? + +**Evidence.** Today the lab pops: 240 px within 3° on grow, 70 px on +stretch (edge scenes `grow`/`stretch`). Figma verification (2026-07-07, +community-evidenced; officially undocumented): fill **stays stored and +active** on rotated children; the engine fills against the **rotated +envelope** in the parent's unrotated axis space; **which child dimension +the fill drives flips at 45°** (observed snap); at exactly 90° at least +one unresolved bug report (text). The aspect-ratio-hack genre depends on +envelope fill (`length = slot/cosθ`). **No continuous-everywhere policy +exists anywhere** — the choice is where the discontinuity lives. + +| option | discontinuity lives at | notes | +| ----------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| **envelope-fill + 45° axis-flip** ← recommended | 45° (same place Figma put it) | Figma-parity class; defined at 90°; no pop at 0°; costs a bounded second per-container solve + conformance rows | +| pure inverse-envelope (always drive main dim) | 90° (driven dim → ∞, clamp is our invention) | continuous elsewhere; diverges from Figma near vertical | +| inert-when-rotated (today's lab rule, promoted) | first degree of the rotate gesture | simplest; single-pass trivially safe; breaks Figma parity + the rotated-spacer import genre; editor should clear-and-offer-restore | +| keep basis + declare pop (E-A4 as-is) | 3° step, 240 px | cheapest; reads worst in the demo | + +**Unblocks.** E-A11/E-A12 spec text, two conformance rows, the +`stretch`/`grow` cells of COMPAT's Figma map. + +## DEC-2 · fill intent on DERIVED kinds (group/bool) — **CLOSED n/a (DEC-0, 2026-07-07)** + +_Closed by the visual-only lock: the configuration this decision +governed no longer exists — sizing never reads rotation, so fill +never fights it (dec0-visual-only.md V-2/V-4). Body kept as the +record of the road not taken._ + +**Question.** A group sits in a flex row with `grow`/`stretch` — today +silently inert. Figma's answer: a stretched group **rescales its +children** (groups resize by scaling). + +| option | notes | +| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **inert-with-report** ← recommended (v1) | derived boxes never take layout-imposed size; report instead of silence; Figma-like rescale stays an explicit editor gesture (K parametric bake); CRDT-clean, reads-never-write preserved | +| resolved-tier visual scale-to-slot | lens-like render-time scale, nothing stored; xywh reads diverge from ink; hit-testing needs the scaled tier | +| K-bake at gesture time | true Figma semantics but a write fan-out across members; reflow-driven slot changes still need one of the above at resolve time | + +## DEC-3 · rotate-gesture escapes: launch-blocking? — **CLOSED n/a (DEC-0, 2026-07-07)** + +_Closed by the visual-only lock: the configuration this decision +governed no longer exists — sizing never reads rotation, so fill +never fights it (dec0-visual-only.md V-2/V-4). Body kept as the +record of the road not taken._ + +**Question.** Are the D3 escapes — rotate-HUD one-click "keep slot +fixed" (wrap/lens re-target) + the E-A7 envelope readout — **v1 ship +requirements** in editor.md, or fast-follows? + +**Evidence.** Figma's forum record shows this exact behavior class +("prevent rotated image growing the bounding box") as a recurring +ticket generator; E1 measured smoothness, not user reaction. +Recommended: **launch-blocking** — the model's boldest choice ships +with its pressure valve. + +## DEC-4 · text determinism posture (blocker B-1) — OPEN + +**Question.** T-3 promises identical measured text geometry +cross-platform within ε — unsatisfiable with real fonts (a 1-ULP +advance difference flips a line count; no ε absorbs a line-height +jump). Pick the posture. + +**Current proof.** With a host font, the engine now gives resolver, drawlist, +and paint one Skia Paragraph-produced immutable glyph layout; paint replays its +glyph IDs and exact list-owned fonts without reshaping. Fontless probes use the +explicitly named stub and emit no glyphs. This closes the architectural +measurement/paint mismatch, not DEC-4: the host-font bridge claims no +cross-platform determinism, deliberately disables fallback, fixes paragraph +direction to LTR, and does not yet provide complete caret mapping. + +| option | notes | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **pin a deterministic shaper** ← recommended | one versioned shaping stack, bit-exact per version; T-3 survives as an INV-per-shaper-version; Figma ships its own text stack for exactly this reason; CRDT/multiplayer + server rendering need stable line counts; cost: we own the pipeline, shaper upgrades become format-versioned events | +| per-platform goldens | T-3 re-scoped to (platform, font stack); cross-platform docs may reflow; cheaper now, divergence surfaces in multiplayer | +| defer, quarantine T-3 | honest but leaves the loudest blocker open; dependent specs cite an unresolved invariant | + +## DEC-5 · coordinate budget (blocker B-2) — OPEN + +**Question.** ULP(1e7) = 1 px, so N-1's "error bounded at ±1e7" cannot +hold in a normative-f32 pipeline. Declare the budget. + +| option | notes | +| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| **declared f32 budget: ±1e6 @ 0.1 px** ← recommended | matches the Skia/engine reality and Figma's accepted-limited-range posture; N-1 rewritten with the honest numbers | +| f64 resolved tier | doubles resolved-tier memory, wasm-boundary cost; Blink-style 1/64 fixed-point is a third path nobody asked for | + +## DEC-6 · bool bounds fork (blocker B-5) — OPEN + +**Question.** `bool` box source: **op-result** bounds (D-5, correct — +subtract-to-empty reports empty) requires render-grade path booleans +inside Phase M (unbuilt, unbudgeted). **Figma reports operand-union.** + +| option | notes | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| **keep D-5 op-result; bool ships when pathops land** ← recommended | no wrong reads ever shipped; bool stays ⛔ until the engine dependency exists | +| operand-union v1 (Figma parity) | cheap now; xywh lies for subtractive ops; migrating later is a breaking read change | +| two named reads (union now, op-result later additive) | honest but two truths for one box — the exact ambiguity a.md §3 exists to kill | + +## DEC-7 · format RMW posture (E-A6 / pedantic C4) — OPEN + +**Question.** FlatBuffers decode→re-encode through an old schema +verifiably **drops unknown fields** (E2), yet conformance M-4 still +claims INV "preserves unknown content through RMW". + +| option | notes | +| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **version-gated writers + M-4 downgraded to POL** ← recommended | writers must be ≥ the document's schema version or refuse; the declared drop-window is zero; simplest honest contract | +| buffer-retention patching | true RMW preservation; substantial format-layer machinery for a lab-stage format | +| leave M-4 as INV | fails as measured — not an option, listed for completeness | + +## DEC-8 · E8 + E9 sequencing — OPEN + +**Question.** Two dealbreaker-class seams were never measured: **E8** +(transpile the E1 document to HTML/CSS, measure the loss class — the +tenant-site web bill) and **E9** (E5-style scanner over the io-figma +corpus: SCALE constraints, rotated-in-auto-layout counts, +`normalizedSize≠size`, boolean nesting, **negative determinants** — +which also quantifies DEC-9's "how often do designs carry mirrors"). + +Recommended: **run both before the phase-3 rewrite** — E9 is cheap +(harness exists), E8 is a day-scale experiment; both feed lock +conditions the rewrite must cite. + +## DEC-10 · the lens's NAME (taxonomy, not spec) — OPEN + +**Question (owner, 2026-07-07).** Doubts about the name `lens`: should +`group` absorb the lens's job, or should it be renamed (`tgroup` / +"transform group")? + +**The genus taxonomy** (why the SPLIT stays regardless of the name): +`frame` OWNS space · `group` IS a set (nothing else — GROUP.md; its +powers: transparent-select, E-A13 pass-through, 3-scalar ungroup bake, +all depend on refusing ops) · `lens` is a VIEW (derived box, ordered +paint ops, quarantine-opaque). A group-with-optional-ops is a two-thing +kind — every consumer forks on "has ops?" and the name gates nothing. +Figma corroborates: they shipped **TransformGroupNode as a separate +node**, not by growing GroupNode. + +**Post-DEC-0 axis check:** "transform group" names the WRONG axis now — +plain groups also transform (header rotation/flip, paint-only). The +lens's real distinction is _structured scalars vs ordered arbitrary +ops_ (shear/matrix/retained-scale/3D-reserved) — the sub-1% quarantine +(E5), rarer still since E-A8 inverted (lens-only-rotate always +redundant). + +| candidate | commits to | verdict | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `lens` (incumbent) | a view: non-destructive, removable, layout-transparent — the metaphor carries the load-bearing semantics; unique, greppable, terse-uniqueness holds | jargon; must be taught once | +| `tgroup` / transform group | group genus — imports FALSE expectations (pass-through? transparent-select? ungroup?); wrong axis post-DEC-0 | fails "refuses the wrong content" | +| `transform` | instantly readable but maximally overloaded — every node colloquially "has a transform"; grep pollution; gates nothing | fails uniqueness | + +**Recommendation:** keep `lens` as the FORMAT name (public commitment, +expensive to change after `.fbs` ships); the UI DISPLAY label is a +separate cheap decision (naming doctrine: public names and surface +labels may diverge — Figma's kiwi names ≠ UI names) and can say +"Transform" if user-testing prefers it. If doubt persists at phase 3, +run the cheap empirical probe: A/B the kind name in the E3 grammar +with cold LLMs and measure misuse (do they expect ungroup? try to give +it layout?). Merging into group is rejected under any name. + +## DEC-9 · resize across zero: wall / slide / flip — OPEN (evidence built) + +**Question.** Drag a resize handle past the fixed edge. The current +model walls (typed reject). Figma flips — uniformly, every node kind, +frames and text included, because it all shares one render transform. +Owner lean (2026-07-07): "actually flip, like SVG — the inner paint +mirrors too — is more correct in 2D graphics; happens quite often, not +critically often." + +**Evidence built today** (the E-A14 arm): `flip_x`/`flip_y` are live in +the lab; the `resize_drag` op re-targets across zero (|extent| + flip +toggle + re-pin; 2–3 writes; **out-and-back = document identity**, +lab-tested); the typed `set_width(−50)` stays a wall +(`NegativeExtent`); flip is layout-invisible for boxed kinds by +construction. 11 tests green ([`lab/tests/flip.rs`](./lab/tests/flip.rs)); +three-policy interactive demo: [`edge-cases/`](./edge-cases) scene +"resize across zero". (Demo provenance: only the flip arm is real +`resize_drag` output; the wall and slide arms are analytic stand-ins +built at their expected geometry.) + +| option | notes | +| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **flip (true mirror, uniform)** ← recommended, matches owner lean | Figma-parity ("same rt"): paint, children, text, image fills all mirror; the only arm where directional content keeps tracking the hand; already implemented + tested | +| slide (re-pin, never mirror) | rect tracks the hand but arrows/image fills/glyphs stay right-way-round — reads broken mid-gesture on any asymmetric content | +| wall (reject at zero) | the pre-decision model; the hand keeps moving, the box doesn't follow | + +**Sub-decision (only if flip):** uniform mirror includes TEXT (glyphs +render reversed — Figma does this; some tools exempt text). Recommended: +**uniform** — exemptions break "one render transform" and make ungroup +bakes kind-dependent. Import note: mirrored `.fig`/SVG content maps to +the flip bits natively (E-A2); E9 will count how often. + +--- + +## Appendix · adopted-by-evidence (veto window, not questions) + +Recorded as decided because peers agree and/or a test now enforces it — +say the word to reopen any of them: + +1. **B-3 uniform clamp-then-re-measure** — min/max clamps re-measure in + both free and flex contexts (both studied peers re-measure). +2. **Law 9 free-context qualifier** — "never moves siblings" holds in + free context; in flow a union change legitimately reflows (Figma + agrees). +3. **min/max clamps bound the box, never the envelope.** +4. **Flip pivot per kind = rotation's rule** (pedantic B1) — center for + boxed/measured, own origin for derived. Now lab-tested (F-3). +5. **Flip composes innermost** (`T·R·F`) — declared + tested (F-2). +6. **Winding reads the stored rotation scalar** — it _is_ intent + (resolves the law-7 / R-E2 / H5 three-way). +7. **−0.0 canonicalized at the write boundary** (R-E3, tested). +8. **NegativeExtent is a typed op error** — extents non-negative stays + bedrock; only the drag gesture re-targets across zero (E-A14). +9. **Declare-only punch list** (resolver reports, no semantics change): + empty-derived placeholder-at-pins; hidden children skipped from + union (MM-6 written); NaN guard over every lens-op scalar; 3D lens + ops = render-skip + preserve; giant-word ink-bounds ⊃ box declared + for hit/cull; empty group spends two gaps in flex (CSS agrees). diff --git a/archive/model-v2/anchor/ENGINE.md b/archive/model-v2/anchor/ENGINE.md new file mode 100644 index 00000000..0d1fc01c --- /dev/null +++ b/archive/model-v2/anchor/ENGINE.md @@ -0,0 +1,470 @@ +# ENGINE — the phase-4 layer programs (day-1 contracts) + +2026-07-08. The five engine layers the migration must shape from day 1 — +each with its **major contracts** (cheap to set now, brutal to retrofit), +its growth path (every stage keeps every contract), and its open studies +(named, instrumented, NOT decided). The model semantics are proven +([`MODEL.md`](./MODEL.md), 114 tests); this document is about the engine +that grows around them. Browser and game-engine technique is adopted +deliberately, not by vibe: the pipeline staging and damage discipline +are the browser's; the storage, spatial, and replay discipline are the +game engine's. + +**Validity rule (standing, H13-style).** A claim enters this document +only under one of five tags — anything untagged is refused: + +- `[MEASURED]` — a number from this workbench (lab, spike, E4 bench) or + a mechanism that exists in the lab today, cited. +- `[PRIOR]` — a finding from the legacy render-opt loop. Transfers as a + **hypothesis** — it was measured on the legacy engine and MUST + re-measure on this model before it binds. +- `[PEER]` — verified browser/game-engine precedent, stated coarsely + enough to be uncontroversial. +- `[CONTRACT]` — normative, testable, binds phase 4. The point of this + document. +- `[OPEN]` — a study question with a named instrument. Decides nothing. + +Decision hygiene: nothing here pre-empts the register — DEC-4/5/6/7/9 +stay owner calls; the contracts below are **decision-independent +sockets** any answer plugs into. + +## The layer map + +| layer | one line | precedent | day-1 contracts | grows into | +| ----- | --------------------------------------------- | ------------------------------------------------- | --------------- | ------------------------------------- | +| ENG-0 | stage purity + the oracle law | browser pipeline staging | 0.1–0.4 | everything below | +| ENG-1 | incremental resolve & damage | dirty bits / query memoization | 1.1–1.4 | dirty-scope → memoized resolve | +| ENG-2 | retained paint: drawlist → tiles → compositor | display lists, damage rects, layers | 2.1–2.4 | damage-driven partial paint | +| ENG-3 | spatial read tier | game broadphase (BVH/LBVH) | 3.1–3.4 | indexed pick/cull/snap at 100k | +| ENG-4 | deterministic content oracles | Figma's own text stack; lockstep float discipline | 4.1–4.5 | pinned shaper, pathops-in-measure | +| ENG-5 | time as data: journal, replay, CRDT seam | input-log replay; per-field LWW multiplayer | 5.1–5.5 | op history → replay rig → multiplayer | + +--- + +## ENG-0 · the meta-layer: stage purity and the oracle law + +The one asset every layer below trades on: **the pipeline is a chain of +pure stages**. `(document + immutable effective values) → resolve → drawlist +→ raster/composite`, plus the read tier (`pick`, spatial queries) off the +resolved columns. An empty effective-value set reads the authored document +exactly. The lab holds the first link as a pure function `[MEASURED]`; phase 4 +extends the chain, never breaks it. + +- **ENG-0.1 · stage purity** `[CONTRACT]` — every stage is a pure + function of the previous stage plus declared inputs. No stage reads + downstream state; no stage writes upstream. (Law 1's "derivable ⇒ not + encodable" is the document-side special case of this.) +- **ENG-0.2 · the oracle law** `[CONTRACT]` — every optimization + (incremental resolve, cache, spatial index, batching, tiling) ships + WITH a differential test against the unoptimized pure stage: + `optimized(input) ≡ reference(input)`, bit-for-bit, over the + conformance scenes and the replay corpus (ENG-5.3). An optimization + without its differential test does not merge. The reference paths are + never deleted — they are the permanent test oracles. +- **ENG-0.3 · order-determinism** `[CONTRACT]` — stages are + deterministic including iteration order (no hash-map-order-dependent + math; SOA columns indexed by NodeId already are `[MEASURED]`). + Determinism is what upgrades every differential test from ε-tolerance + to bit-equality — ε hides real bugs. +- **ENG-0.4 · this document's own gate** `[CONTRACT]` — additions to + ENGINE.md carry a validity tag or are rejected; `[PRIOR]` entries + must name their re-measurement before binding. + +**Never.** No layer may introduce hidden mutable state "for +performance" without routing it through ENG-0.2. A cache that cannot be +differential-tested is a bug factory with a speedup attached. + +--- + +## ENG-1 · incremental resolve & damage + +**Precedent.** Browsers keep style/layout/paint validity per phase with +dirty bits and scoped relayout `[PEER]`; rust-analyzer's salsa +demonstrates query-memoization as the alternative shape `[PEER]`; game +engines propagate dirty transforms through subtrees with change +detection `[PEER]`. + +**Evidence on file.** Resolve-per-frame is the proven baseline: starter +scene 0.008 ms resolve / 0.17 ms frame; 10k nodes 0.35–0.47 ms resolve +`[MEASURED]`. Locality is bounded — a leaf edit re-resolves to the +nearest fixed-extent ancestor, ~18 µs per card subtree under clean +parent boxes `[MEASURED]`. The lab's per-container hug chain is +2^depth — phase 4's single-tree layout removes it structurally +`[MEASURED]` (LIMITS cost note). The incremental engine is an +**optimization to add, not an architecture to build first** — that +ordering is itself the day-1 decision, already made and proven by the +spike. + +**Major contracts.** + +- **ENG-1.1 · the full resolver is the eternal oracle** `[CONTRACT]` — + incremental resolve must produce a resolved tier bit-identical to + from-scratch resolve, for every edit, forever. CI runs the + differential over the conformance scenes + replay corpus. (This is + ENG-0.2 applied; it is listed separately because it is the contract + most engines break first.) +- **ENG-1.2 · ops declare their dirty class** `[CONTRACT]` — the op + layer already enumerates typed write-sets (write counts are law + `[MEASURED]`); extend each op with a declared invalidation scope: + which phases (M/L/T/B) × which extent (self · subtree · + measure-chain-to-fixed-ancestor · bounds-only). Day 1 the enum merely + exists and the engine ignores it (full resolve); the incremental + engine consumes it later without re-auditing every op. CSS + containment is the browser's version of making damage scope explicit + `[PEER]`. +- **ENG-1.3 · viewport is root context only** `[CONTRACT]` — resolved + output for a node never depends on scroll/zoom/culling. Culling is a + paint/read concern (ENG-2, ENG-3). Violating this makes every pan a + full invalidation and poisons ENG-1.1. +- **ENG-1.4 · caches are a separate, keyed tier** `[CONTRACT]` — any + memoized value lives outside the document and the resolved tier, + keyed by generational identity (ENG-2.3). The document and Resolved + stay plain values. + +**Growth path.** full-per-frame (today, proven) → dirty-scope +re-resolve consuming ENG-1.2 (the 18 µs bound is the promised payoff) +→ memoized sub-queries where profiling justifies. + +**Open studies.** OS-1a: dirty-bit propagation vs salsa-style +memoization vs hybrid — instrument: the E4 bench harness + replay +corpus, measured at 10k/100k `[OPEN]`. OS-1b: does Taffy's tree API +admit subtree-scoped relayout cleanly, or does the flex phase stay +whole-tree longer than M/T/B? — instrument: prototype on the lab's flex +scenes `[OPEN]`. + +--- + +## ENG-2 · retained paint — display list, damage, tiles, compositor + +**Precedent.** The browser pipeline: paint produces a display list; +raster consumes it in tiles; a compositor owns layers and frame pacing; +damage rects flow forward `[PEER]`. Game render graphs contribute +batching/culling vocabulary `[PEER]`. + +**Evidence on file.** The spike is paint-bound, not resolve-bound: 10k +nodes = 0.35 ms resolve vs ~9 ms paint `[MEASURED]` — paint is where +the next order of magnitude lives, which is precisely why this layer +exists. The legacy loop's findings transfer as hypotheses only +`[PRIOR]`: unstable-full-draw suppression won big (−71%), analytic +mask-filter shadows halved stable full draws, and **naive layer +promotion/demotion was falsified twice** — re-measure all three on this +model before adopting. + +**Major contracts.** + +- **ENG-2.1 · drawlist is a pure stage** `[CONTRACT]` — the same immutable + effective model view plus its `Resolved` product deterministically projects + one diffable drawlist, exactly like resolve (ENG-0.1). The raster executor + reads only that drawlist and declared resources; it never re-reads authored + properties or independently applies effective values. A complete frame + checks that the resource-environment incarnation and revision still match + the key captured at build before replay begins. +- **ENG-2.2 · damage flows forward only** `[CONTRACT]` — authored op or + effective-value change → declared impact class → resolved-tier diff plus + drawlist diff → screen rects. Resolved state, the ordered drawlist, and the + declared resource-environment revision travel as one immutable frame + product; complete damage compares two products rather than separately + pairable parts. Changing bytes or readiness under one logical RID must + invalidate retained raster state and conservatively damage its consumers. + The impact class may skip work only after an optimization proves equality + with the complete product diff. No stage invents damage or silently drops a + paint-only change. Damage is data, loggable and assertable per frame. +- **ENG-2.3 · arena-scoped generational cache keys** `[CONTRACT]` — cache + identity is `(arena incarnation, slot, generation)`, never a bare NodeId or + a slot/generation pair detached from its arena. Slot generation prevents + reuse inside one arena; arena incarnation prevents a fresh materialization + or divergent document clone from validating an old key whose slot and + generation happen to match. Generation increment never wraps. The runtime + identity remains a storage artifact, not authored identity. +- **ENG-2.4 · the compositor owns pacing** `[CONTRACT]` — one frame + entry point schedules input → resolve → paint → present; hosts adapt + to it, not vice versa. The legacy `FrameLoop` unification is the + in-repo precedent `[PRIOR]` — its lesson (fragmented tick/redraw + paths rot) binds; its implementation does not. + +**Growth path.** immediate paint from SOA (today) → display list + +damage rects (partial repaint) → tiled raster (infinite canvas at deep +zoom) → composited layers, promotion by measured policy only. + +**Open studies.** OS-2a: tile scheme for an unbounded, deeply-zoomable +canvas — zoom-pyramid vs single-scale re-raster; instrument: `--bench` +scenes at extreme zoom `[OPEN]`. OS-2b: layer promotion heuristics — +re-run the legacy falsification on this engine before shipping any +promotion `[PRIOR→OPEN]`. OS-2c: text raster caching interaction with +ENG-4 versioning (glyph atlas keyed by oracle version) `[OPEN]`. + +**Never.** Paint must not become a second place where model semantics +live — a drawlist item renders what the resolved tier says, or the +resolved tier is wrong. + +--- + +## ENG-3 · the spatial read tier — one query API over a broadphase + +**Precedent.** Game engines split broadphase (index over AABBs) from +narrowphase (exact tests) and rebuild LBVHs per frame via morton sort +at large N `[PEER]`. Browsers hit-test by paint-order tree walk — the +editor case (marquee, snap, cull, minimap) outgrows that quickly +`[PEER]`. + +**Evidence on file.** The read tier is already SOA: `world_aabb` is a flat +column indexed by NodeId `[MEASURED]` — literally the input array a broadphase +wants. Resolution also snapshots child order, transparent-select behavior, and +effective descendant clips. `pick` consumes only that resolved frame and owns +the oriented inverse-world test, lens post-ops, transparent-select promotion to +the outermost derived, and the linear walk `[MEASURED]` — correct, and the +permanent narrowphase + oracle. + +**Major contracts.** + +- **ENG-3.1 · one query API** `[CONTRACT]` — point-hit, rect + (marquee), viewport cull, and nearest-edge/center (snap candidates) + are one API family. Consumers (HUD, marquee, snapping, culling, + minimap, pick) never hand-roll tree traversals. Day 1 the API fronts + the linear walk; the index slots in behind it unchanged. +- **ENG-3.2 · the walk is the oracle** `[CONTRACT]` — every index + answer is differential-tested against the linear reference (ENG-0.2). + Broadphase may over-approximate (candidates), never under-approximate + (misses). +- **ENG-3.3 · model laws live in the query layer** `[CONTRACT]` — + paint-order topmost, transparent-select promotion, lens post-ops + hit-testing, hairline slop: these are semantics, and they stay in the + narrowphase (today's `pick.rs`), NOT in consumers and NOT in the + index. An index swap must never change what gets selected. +- **ENG-3.4 · spatial reads read the read tier only** `[CONTRACT]` — + the index and narrowphase read resolved columns and the resolved traversal/ + clip snapshot, never a separately pairable document or effective-value view. + The index rebuilds/refits from a resolve diff (ENG-2.2's damage feed), so it + can never disagree with what was resolved. + +**Growth path.** linear walk behind the API (today, correct) → +per-frame rebuilt BVH over the SOA column (morton/LBVH — the column +layout makes the sort nearly free `[PEER]`) → refit-vs-rebuild policy +at scale. + +**Open studies.** OS-3a: rebuild-vs-refit threshold at 10k/100k under +resolve-per-frame — instrument: bench scenes + the replay corpus +`[OPEN]`. OS-3b: snap-candidate query shape (k-nearest edges vs range +query + filter) measured against real gesture traces `[OPEN]`. + +--- + +## ENG-4 · deterministic content oracles — text, pathops, images + +**Precedent.** Browsers are deliberately NOT cross-platform +deterministic in text; Figma ships its own text stack precisely to be +`[PEER]` (already cited as evidence in DEC-4). Lockstep game +simulations pin float operation order and instruction choices to keep +replicas bit-identical `[PEER]`. + +**Evidence on file.** The text seam is concrete: the resolver accepts an +explicit `TextLayoutOracle`; the host-font frame path uses Skia Paragraph to +produce one immutable final-width glyph layout; the drawlist gives that same +artifact to the fill and every stroke; and paint replays its glyph IDs and +exact list-owned fonts without reshaping `[MEASURED]`. The artifact records its +oracle/environment, input constraint, assigned box, semantic font identity, +line topology, and logical/ink bounds; bounds and damage consume it directly +`[MEASURED]`. Fontless probes use the explicit `stub@lab-0`, which emits line +metrics and no glyph runs +`[MEASURED]`. This proves shared consumption for the implemented sizing, +painting, bounds, and damage paths—not cross-platform determinism or universal +conformance. DEC-4 remains open; the current bridge deliberately disables font +fallback and fixes paragraph direction to LTR `[MEASURED]`. Pick remains +box-based, while editing/caret geometry and faithful export are not +implemented. B-1 (wrap decisions are discrete; no ε absorbs a line-height +jump) and B-5 (bool needs pathops inside measure) remain the two blockers this +layer services `[MEASURED]` (LIMITS). + +**Major contracts** (decision-independent: DEC-4 picks the shaper, +DEC-5 the numbers, DEC-6 the bool posture — these sockets hold under +any answer): + +- **ENG-4.1 · oracles are explicit inputs** `[CONTRACT]` — nothing + inside resolve reaches for ambient fonts/resources/platform metrics. + Already true; stays true. +- **ENG-4.2 · oracles are versioned** `[CONTRACT]` — the oracle version + is document-visible; an oracle upgrade is a format event, never a + silent drift. Golden corpora per version; CI pins each version's + outputs. +- **ENG-4.3 · determinism posture is explicit** `[CONTRACT]` — oracle + identity names the comparison domain. DEC-4 decides whether that domain is + cross-platform bit-exact or platform-scoped; until then, the Skia Paragraph + bridge claims neither. ENG-1.1 and ENG-5.2 use bit-equality only within the + declared oracle environment. +- **ENG-4.4 · the numeric profile is declared** `[CONTRACT]` — there IS + a documented profile (precision, range, operation-order rules) the + resolver and oracles conform to; DEC-5 fills in the numbers. Bugs + against the profile are bugs; drift outside it is undefined, not + quietly tolerated. +- **ENG-4.5 · measure-phase oracles are budgeted and cacheable** + `[CONTRACT]` — phase-M oracle calls (shaping, pathops) are keyed by + `(content, constraints, environment, oracle version)` for memoization under + ENG-1.4, and their cost is measured per frame — the phase-M budget is + a number, not a hope. + +**Growth path.** explicit fontless stub + host-font Skia Paragraph bridge +(today, measured) → the pinned or platform-scoped production oracle chosen by +DEC-4 → pathops in phase M with an ENG-4.5 budget (unblocks B-5/bool) → +image/decode metrics under the same versioning. + +**Open studies.** OS-4a: shaper candidates under the ENG-4.3 constraint +— instrument: cross-platform golden corpus diff (feeds DEC-4, does not +make it) `[OPEN]`. OS-4b: pathops-in-measure cost at realistic bool +nesting — instrument: E9's corpus counts + bench `[OPEN]`. + +**Never.** Oracle output never enters the document (law 1 already +forbids it); an oracle upgrade never rewrites stored intent. + +--- + +## ENG-5 · time as data — transactions, journal, replay, CRDT seam + +**Precedent.** Game engines treat the input log as a first-class +artifact: deterministic replay for repro, regression, and benchmarks +`[PEER]`. Figma's published multiplayer write-ups describe per-property +last-writer-wins and client-local undo `[PEER]`. + +**Evidence on file.** The op layer is already journal-shaped: typed, +delta-form, enumerated write-sets, "a drag ends as if written once" is +law `[MEASURED]`. The spike ships snapshot undo (documents are values) +`[MEASURED]`. The C-matrix demonstrated field-level merges in tests but +has NEVER met a real replicated backend — named in the REPORT lose +column `[MEASURED]`. n0 XML Version 4 now defines durable structured source +addresses separately from arena-scoped runtime keys `[MEASURED]`; the current +operation and replay wires remain slot-addressed and therefore in-process +only. + +**Major contracts.** + +- **ENG-5.1 · transaction = gesture** `[CONTRACT]` — the journal's unit + is the typed op; a gesture groups ops into one transaction with + all-or-nothing history semantics. The existing write-count law is the + enforcement instrument. +- **ENG-5.2 · replay determinism** `[CONTRACT]` — a replay file is + `(initial document, op log)`; playing it back yields a bit-identical + document AND resolved tier (stands on ENG-0.3 + ENG-4.3/4.4). This is + the property that turns "it crashed once" into a fixture. +- **ENG-5.3 · one corpus, four consumers** `[CONTRACT]` — the replay + format is THE shared artifact: bug repro, perf bench input, fuzz seed + (structure-aware op fuzzing), and conformance fixture. No parallel + bespoke trace formats. +- **ENG-5.4 · stable identity precedes distribution** `[CONTRACT]` — + distributed ops address nodes by stable authored identity. Session-local + slot ids suffice for in-process history. Version 4 locks the source and + component-occurrence address shape, but no cross-session replay, + persistence-of-history, or multiplayer claim exists until operation and + replay schemas deliberately adopt that contract. Those features remain + walled, not inferred from the source parser. +- **ENG-5.5 · snapshots are the honest floor** `[CONTRACT]` — history = + invertible op log where proven, document snapshots where not (the + spike's posture). An op is only "invertible" once its inverse is + property-tested (`apply(op); apply(inverse(op)) ≡ identity` — the + E-A14 out-and-back test is the template `[MEASURED]`). + +**Growth path.** snapshot undo (today) → op journal + transactions → +replay rig wired into CI (ENG-0.2's corpus) → CRDT seam: run the +C-matrix against a real backend; only then multiplayer semantics +(undo-locality etc.) become design work. + +**Open studies.** OS-5a: journal-vs-snapshot memory/latency crossover +at real document sizes — instrument: replay corpus + bench `[OPEN]`. +OS-5b: C-matrix vs a real backend — which merge rows survive contact +`[OPEN]`. OS-5c: multiplayer undo semantics — study Figma's +client-local model against the op journal `[PEER→OPEN]`. + +--- + +## The cross-cutting instrument — the conformance rig + +Every layer above is only cheap because ENG-0 holds; the rig is how it +keeps holding: differential tests (incremental vs full, index vs walk, +optimized paint vs reference), structure-aware op fuzzing over the +replay format, golden corpora per oracle version, and WPT-style +external pinning where a peer is the oracle (flex conformance pins to +Chromium, never to Taffy — the E4 lesson `[MEASURED]`; `grida_wpt` +is the in-repo precedent for the discipline `[MEASURED]`). + +## SETUP — the ground contracts (S-1…S-7) + +The five layers above are the runtime; these are the ground they sit on +— the workspace shape, the boundaries, and the rig. A **day-1 skeleton +now implements them** ([`../engine/`](../engine) — `anchor-engine`, +consuming `anchor-lab`; the spike is re-hosted onto it and the gate is +green), so several of these are `[MEASURED]` rather than aspirational. + +- **S-1 · crate topology & the Stylo landing** `[CONTRACT]` — the model + crate (`anchor-lab` today) carries no host deps and compiles for every + target incl. server; the engine embeds into the host component-wise. + The one industry rewrite that shipped did it this way — Stylo and + WebRender landed _into_ Gecko; greenfield "new browser" rewrites did + not `[PEER]`. So the lab stays the greenfield core, kept + dependency-clean, and promotion to `crates/` is a `git mv`, not a + rewrite `[MEASURED]`. Phase-4 landing order: resolve+drawlist behind + the pipeline seam first, then pick/query, then interaction/journal — + each gated by shot+replay parity. The spike re-host **is** this + landing, rehearsed `[MEASURED]`. +- **S-2 · dependency-direction law** `[CONTRACT]` — arrows never reverse: + model (no skia/winit/wasm-bindgen) ← engine (model with skia) ← hosts (engine + with winit/egui/GL). Raster semantics stay in `paint.rs`; shaping stays in + `text_layout.rs`; frame/cache/host code may own or route Skia surfaces and + resources without duplicating either semantic stage. Compiler-enforced, not + reviewer-enforced. Proven: `anchor-lab` builds feature-less, + `anchor-engine` adds only skia, the spike holds all chrome `[MEASURED]`. +- **S-3 · one write door, one read door** `[CONTRACT]` — every mutation + is a typed `Op` through `apply` (the journal's unit); every read is + the resolved tier (via `query`) or the text IR. No tool reaches into + node fields. The legacy's three-answers-to-one-question split-brain is + what reach-ins grow into. Proven: the spike's interaction sites all + route through `apply`; pick/hover through `query` `[MEASURED]`. +- **S-4 · wasm-first mechanics** `[CONTRACT]` — the core is + single-thread-first (wasm threads need cross-origin isolation; + parallelism is a native bonus behind ENG-0.3, never load-bearing) + `[PEER]`; no interior mutability in the model/resolved tiers (value + semantics also make snapshot undo and replay cheap — the document is + one value `[MEASURED]`); the arena/SOA layout suits linear memory that + only grows `[PEER]`; the boundary is ops in, damage/query results out + (the C-ABI seam), never "JS reaches into engine memory." +- **S-5 · rig before engine** `[CONTRACT]` — the gate exists _before_ + the engine grows: shots byte-compare, replay determinism, resolve+build + budgets against a machine-tagged baseline. Pixel goldens raster on CPU + (GPU AA varies by machine — browsers golden on software raster + `[PEER]`). The first untested "temporary" optimization fails here. + Built with the skeleton (`bin/gate.rs`, green) `[MEASURED]`. +- **S-6 · gated observability** `[CONTRACT]` — per-stage spans/counters + compile to zero when off (the `trace` feature); the profiler must + never distort the profile (the legacy loop measured exactly this — + devtools cost-prediction polluted plan build until gated `[PRIOR]`). + Proven: `t_span!`/`t_count!` expand to the bare block with `trace` off + `[MEASURED]`. +- **S-7 · one shipping profile** `[CONTRACT]` — both DEC-0 arms stay + implemented and tested, but the engine ships ONE profile; arms and + flags exist for tests/studies only, and CI enumerates the supported + configuration set. The guard against the legacy's 26-arm branch forest + regrowing as feature flags — the lab pins both arms in tests while the + default resolves one `[MEASURED]`. + +Two one-liners that also belong on day 1: + +- **Format carries a version field from byte one** `[CONTRACT]` — the + refuse-newer posture is DEC-7's call; the field itself is not optional. + The `.replay` header already stamps magic + version, the pattern + rehearsed `[MEASURED]`. +- **DPR / pixel-snapping is a declared paint policy** `[CONTRACT]`, not + rounding scattered through code — the executor takes the view + transform and the host owns DPR (the spike passes `dpr` into the HUD; + the scene view composes it) `[MEASURED]`. + +## Scope fence (named, not silent) + +- **Above Skia.** Raster stays Skia; this document pioneers the + architecture above it. No custom GPU path rendering, no rasterizer + replacement — the differentiation is in the five layers, not under + them. +- **Not ECS.** The arena/SOA is a storage layout, not a component + model. Nodes keep their typed header+payload shape (the model's + strict-states law); nothing here licenses arbitrary + component attachment. +- **Relation to the register.** ENG-4 services DEC-4/DEC-5/DEC-6 + without deciding them; ENG-5.4 depends on the a.md §12 stable-id + item; ENG-2's `[PRIOR]` entries cite the legacy render-opt loop and + bind only after re-measurement here. diff --git a/archive/model-v2/anchor/GROUP.md b/archive/model-v2/anchor/GROUP.md new file mode 100644 index 00000000..9bdbfa4f --- /dev/null +++ b/archive/model-v2/anchor/GROUP.md @@ -0,0 +1,150 @@ +# GROUP — what a group _is_ + +2026-07-07. The owner's framing: groups are needed in a graphics tool, +no questions asked — but every system means something different by one. +SVG's `` is a transform + a style-inheritance scope; HTML has no +group at all (a div is a real box that always contributes to layout); +Figma's group "only holds" — and its children's constraints famously +bind to the **non-group parent**. This chapter fixes the definition for +anchor, documents what is already decided and tested, and resolves the +one open fork with verified peer semantics (research run 2026-07-07, +primary sources; quotes below). + +## The definition + +> **A group is a named set with a coordinate space — nothing else.** +> It has no box of its own (derived union), no style, no layout opinion. +> Everything a group appears to "have" is either carried in the uniform +> header (opacity, blend, rotation, placement of its origin) or derived +> from its members. + +## The peer table + +| | box | children's coords | constraints ref | AL/layout pass-through | style inheritance | transform unit | +| ------------------ | ---------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------- | +| SVG `` | none (bbox derived) | group space | n/a | n/a | **yes — fill/stroke cascade** | transform attr | +| HTML div | **real box** | parent space | n/a | it IS layout | CSS cascade | transform (paint-only) | +| Figma group | derived, re-fits (`resizeToFit`) | **stored** group-relative; **presented** frame-relative (plugin API skips groups) | **nearest FRAME — skips groups** (verified) | **no** — layoutAlign/Grow inert on group children; the GROUP is the flex item and carries FIXED/FILL itself (verified) | no | stored on the group-frame; re-fit writes compensate | +| Sketch classic | derived | group space | **the group** (opposite of Figma) | n/a | no | frame + rotation | +| Sketch Athens | derived | _frame_-relative (groups skipped, Figma-like) | frame pins | via Frames | no | | +| **anchor `group`** | derived union of oriented children (D-1) | group space | direct parent today → **E-A13 proposal: nearest non-derived ancestor** | **no** (= Figma, verified parity); the group carries `grow`/`self_align` itself | **no — rejected by construction** (empty payload) | 3 scalars (rotation + origin), never child writes | + +Key verified quotes (Plugin API / help center / best-practices): + +- _"Constraints of this node relative to its containing FrameNode … + Group and BooleanOperation nodes do not have a constraint property + themselves. Instead, resizing a frame applies the constraints on the + children of those nodes."_ +- _"Constraints applied to elements will always be relative to the + closest parent frame — not relative to the bounds of the group."_ +- _"layoutAlign … applicable only on direct children of auto-layout + frames"_ — group children do **not** negotiate with the outer frame; + the user's recollection is half-right: **constraints pass through; + auto-layout child properties do not** (the group itself is the item). +- _"It is **not** relative to its direct parent if the parent is a + group"_ — Figma **stores** group-relative (kiwi: groups are FRAME + nodes, `resizeToFit: true`) and **materializes** frame-relative in the + plugin API. The same stored-vs-presented split as `normalizedSize` + (E7) — Figma keeps validating our reads-materialize doctrine. +- _"a group's position and size will change if you change its content"_ + — Figma pays **re-fit compensation writes** on every child edit + (X-FIG-4), the exact behavior anchor's derived-origin design exists + to avoid (D-3: child edits write nothing to the group — lab-tested). + +## Decided and tested (the closed half) + +| decision | rule | evidence | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| Box | derived = union of **oriented** children; op-result for `bool` | D-1 ✅, `group_union_of_rotated_members` ✅ | +| Placement | bindings place the **origin**, never the union (E-A1) | D-2 ✅ `d2_sibling_stability…`, depth-3 ✅ (census fix) | +| Child edits | never write the group — reported box changes, siblings don't move (free ctx) | D-3 ✅; law 9 with the census's in-flow qualifier | +| Rotation | one scalar, origin pivot; center-feel gesture = 3 writes; ungroup bakes per-kind | ✅ `group_origin_pivot…`, `ungroup_nested_group…` (census fix) | +| Style inheritance | **rejected** — group payload is empty; no fill/stroke can exist on a group, so nothing can cascade (the SVG `` behavior is structurally impossible, which is the strongest form of "not what we want") | model shape (a.md §3) | +| Compositing | group opacity/blend live in the header (isolate-then-fade); **effects ignored-by-rule** — a Figma group with effects re-kinds to frame at import | §8; COMPAT figma row | +| In flow | the group is one flex item via its derived AABB (rotated per E1) | D-6 ✅ `d6_group_in_flex…`, edge demo `group` scene | +| Empty / hidden | empty union = zero rect at the placed origin; hidden children skipped | ✅ census tests ×2 | +| SVG `` import | → group natively; `` re-kinds to frame | COMPAT svg row | + +## The fork, resolved: constraint transparency (E-A13, proposal) + +**Problem.** Anchor today: bindings resolve against the _direct_ parent, +and E-A5 makes End/Center/Span error-by-rule under a derived parent — +so a right-pinned icon inside a group **loses its responsive intent**. +Figma: constraints skip groups to the nearest frame. Faithful import +currently requires group→frame promotion (COMPAT surgery row), trading +away either grouping or responsiveness. + +**Why Figma's answer looked cyclic for us — and isn't.** The naive +pass-through reading ("resolve the child against the outer frame, then +derive the group") threatens a cycle: child position → union → group +box → child position. Figma escapes by _writing_ re-fits back into the +stored group-frame (compensation writes). **Anchor escapes by E-A1**: +the group's origin is _stored intent_ (Start-pin scalars), independent +of the union. So pass-through resolution is single-pass: + +``` +E = extent of the nearest non-derived ancestor A (known, top-down) +x_in_A = binding table against E (Pin{End,o} → E − o − w) +x_in_grp = x_in_A − Σ origins of the derived chain (stored scalars — known) +union = derived afterward, as always +``` + +No re-fit writes, no cycle, no CRDT cost — the derived-origin rule we +adopted for sibling stability turns out to be the _enabling_ condition +for Figma-parity constraint transparency. (This is the run's second +case of one decision paying for another; the first was rotation's +center pivot making Pin{Center} correction-free.) + +**E-A13 (for the phase-3 spec):** + +1. Binding **reference** = the nearest non-derived ancestor's box. + Under a derived parent, non-Start pins resolve through the chain as + above — E-A5's error-by-rule shrinks to the truly underdetermined + cases (derived chain under an Auto-hug axis, and free-standing + groups with no extent-bearing ancestor… which the regularized root + guarantees cannot happen). +2. **v1 restriction:** the derived chain must be unrotated and + unflipped for non-Start pins (mixing a rotated group space with the + ancestor's axes has no honest single answer — Figma is equally + undefined there). Violation = error-by-rule with report, exactly the + E-A5 mechanism, narrowed. +3. **Auto-layout properties do NOT pass through** — verified Figma + parity. `grow`/`self_align` on group children under an outer flex + are inert (§8 gains the explicit cell); the group itself carries + them (with the census caveat that grow/stretch on a derived box + needs its declared rule — the Figma answer is rescale-children, + which for anchor is a K-bake, kept op-layer). +4. **COMPAT upgrade:** the figma "group constraint-transparency" row + moves surgery → native; the group→frame promotion remains only for + effects-carrying and rotated-constrained groups. +5. Conformance: new G-rows (pin-through-group resolution table; + chain-rotated → report) + a D-row (pass-through preserves D-2 + sibling stability — it does, by construction, but it must be locked + by test). + +**Costs, honestly:** resolution must walk the derived chain (bounded, +cheap); the §8 matrix gains a context column ("under derived parent"); +the text IR grammar needs one sentence; and the group-resize _gesture_ +(user drags a group handle) remains what it is everywhere — an op-layer +fan-out (Figma: constraint-apply; Sketch K: scale; anchor: per-child +bake with declared write-set), never a stored mode. + +## The lens corroboration + +Figma is beta-testing **`TransformGroupNode`** — "a group with +`transformModifiers` that transforms its children." That is a +_non-transparent_ group: exactly anchor's `lens`, invented independently +by the peer with the largest corpus. The group/lens split (transparent +set vs transforming quarantine) is not our idiosyncrasy; it is where +the industry is heading. Worth tracking for import (a TransformGroup +imports as a lens, trivially). + +## Residual opens + +- Rotated-chain pass-through (declared error in v1; a future answer + needs a "constraints in rotated spaces" doctrine nobody has). +- `bool` inherits all group placement rules but its operand semantics + and Phase-M path-ops dependency stay in LIMITS §B-5. +- The group-resize gesture write-set (op-layer, editor.md row owed — + Figma's `resize()` vs `resizeWithoutConstraints()` split is the + precedent to mirror). diff --git a/archive/model-v2/anchor/LIMITS.md b/archive/model-v2/anchor/LIMITS.md new file mode 100644 index 00000000..e1c92c0e --- /dev/null +++ b/archive/model-v2/anchor/LIMITS.md @@ -0,0 +1,148 @@ +# LIMITS — the ship-readiness census + +2026-07-07. Every kind × mechanism either has a passing test, a declared +rule, or a **named** limitation — no silent unknowns. Sources: the lab +suite (**92 tests** after this census + the E-A14 flip arm), the edge demo sweeps, the +three-domain enumeration run (48 structured edges), and the pedantic +review. Legend: ✅ proven (lab test) · 🎬 demo'd (edge-cases page) · +📜 declared, untested · 🔶 SPEC-SILENT (policy owed) · ⛔ known +limitation (not built / can't hold as stated). + +> **DEC-0 second lock (2026-07-07): the default is VISUAL-ONLY** — +> in-flow rotation is paint; sizing is CSS-pure +> ([`dec0-visual-only.md`](./dec0-visual-only.md)). The rotation/flow +> punch-list items below (E-A11/E-A12, grow/stretch-on-derived, law-9 +> qualifier) are CLOSED n/a under the new default; they remain accurate +> for the documented alternative arm. + +## The kind × mechanism matrix + +| kind | box | realization | rotation | flip | in flow | as parent | quarantine | notes | +| ------ | ------------- | --------------------------------- | ----------------- | -------------------------------- | -------------- | ----------- | ---------- | ------------------------------- | +| frame | ✅ | ✅ children/flex | ✅ rigid (🎬) | 📜 (boxed path, F-1 class) | ✅ | ✅ | — | | +| shape | ✅ | ✅ parametric + free children | ✅ | ✅ (F-1/2/4 + 🎬) | ✅ | ✅ | — | rect/ellipse/line only in lab | +| text | ✅ | ✅ flowed (oracle-backed) | ✅ (🎬) | 📜 mirrors uniformly (DEC-9 sub) | ✅ | n/a | — | Skia Paragraph path; B-1 open | +| vector | ⛔ not in lab | 📜 mapped (E-A9, write side open) | 📜 | 📜 flip bits, points untouched | 📜 | n/a | — | B2 lock set before phase 3 | +| image | ⛔ not in lab | 📜 fitted (engine ships BoxFit) | 📜 | 📜 paint mirrors (DEC-9) | 📜 | n/a | — | low risk: paint seam exists | +| embed | ⛔ not in lab | 📜 flowed | 📜 | 🔶 | 📜 | n/a | — | the live-web valve (COMPAT) | +| group | ✅ | ✅ union (oriented) | ✅ origin pivot | ✅ origin pivot (F-3/F-6) | ✅ (🎬) | ✅ | — | transparency fork → GROUP.md | +| bool | ⛔ not in lab | ⛔ op-result box unproven | 🔶 pivot unstated | ⛔ | 🔶 | 🔶 operands | — | needs path ops in Phase M | +| lens | ✅ | ✅ ops (2D) | ✅ | 🔶 order vs ops | ✅ pre-ops box | ✅ | ✅ | 3D ops = encoding-reserved only | +| tray | ⛔ not in lab | 📜 (≈frame, no clip/layout) | 📜 | ⛔ | 📜 | 📜 | — | root-treatment open (a.md §12) | + +## Fixed during this census (bugs found by construction, now regression-locked) + +1. **Span-fill text never re-wrapped** — `Span{0,0}` (the canonical fill) + ignored as a wrap constraint → single-line fill text. Fixed in the + resolver; `span_fill_text_rewraps` ✅. +2. **Nested derived unions swallowed the inner union offset** — a + group-in-group's content landed 20px off (D-E2 violation). Fixed; + `nested_group_union_offset_exact` ✅. +3. **Hug used the wrong pivot for rotated derived children** + (center-concentric shortcut vs origin pivot). Fixed — hug now + transforms each child's true local AABB; `hug_wraps_rotated_group_exactly` ✅. +4. **Ungroup bake was wrong for derived children** (center formula on an + origin-pivot kind). Fixed with a per-kind bake; + `ungroup_nested_group_preserves_world` ✅. +5. **−0.0 rotation passed the R-E3 guard** — now canonicalized at the + boundary; `negative_zero_rotation_canonicalized` ✅. + +### Closed after the census — the flip arm (E-A14, 2026-07-07) + +The owner-raised cross-zero red flag forced flip from "decided, zero +code" to built: pivot-per-kind (B1) tested, `T·R·F` innermost declared + +- tested, layout-invisibility proven, `resize_drag` re-target with + out-and-back identity, `NegativeExtent` typed wall (a **missing guard + found**: `set_width(−50)` was silently accepted before), flip-aware + ungroup bake (σ conjugation), IR round-trip. 11 tests in + [`lab/tests/flip.rs`](./lab/tests/flip.rs); the wall/slide/flip gesture + policy is [`DECISIONS.md`](./DECISIONS.md) **DEC-9**. + +## The blockers (decide-before-ship; none is quietly deferrable) + +- **B-1 · Text determinism vs real shaping.** T-3 (INV: identical + measured geometry cross-platform within ε) is **unsatisfiable under + tolerance semantics** once real fonts arrive: wrap decisions are + discrete — a 1-ULP advance difference flips a line count and jumps + geometry by a full line-height no ε can absorb. Figma ships its own + text stack for exactly this reason. Decision owed: pin a deterministic + shaper (bit-exact policy) or re-scope T-3 to per-platform goldens. + The lab's exact metric made E1–E4 evidence structurally blind to this. The + engine now proves one shared measurement/draw artifact with a host-font Skia + Paragraph path, but that bridge is not cross-platform deterministic and + records only a process-local font environment. It deliberately has fallback + disabled, accepts only a width constraint, fixes direction to LTR, and + reports unresolved glyphs without yet returning the RFD's typed resolution + failure. It closes the architecture mismatch, not B-1 or DEC-4. +- **B-2 · f32 at far-canvas.** ULP(1e7) = 1.0px, so N-1's "error bounded + at ±1e7" cannot hold in a normative-f32 pipeline (Blink uses 1/64 + fixed-point; Figma accepts limited range). Decision owed: declared + coordinate budget (e.g. ±1e6 @ 0.1px) or f64 resolved tier. +- **B-3 · min/max × measured re-wrap.** Free context clamps _without_ + re-measure (spec-faithful, peer-less: ⅔ of ink can leave the box); + flex context re-measures via the layout engine. One node, two heights + by parent mode. Both peers re-measure. Decision owed: clamp-then- + re-measure as the uniform rule (amend "clamp last"). +- **B-4 · E-A9 write side** (pedantic B2, unchanged): stationarity, + ref-rect intent status, zero-extent axes, bounds operator ("network + bounds" = vertex hull vs curve extrema vs control hull — undefined; + SVG getBBox says tight-geometry, control points excluded). +- **B-5 · bool in Phase M.** Op-result bounds require render-grade path + booleans inside _measure_ — an engine-architecture dependency (pathops + in the resolver), unbuilt and unbudgeted. Plus the COMPAT fork: + Figma reports operand-union bounds, D-5 says op-result. + +## Policy owed (🔶 SPEC-SILENT, enumerated — the phase-3 punch list) + +Rotation/flow: rotated×stretch & re-measure (E-A12) · grow×rotation +continuity (E-A11) · grow/stretch on derived kinds (silently inert +today; Figma's answer is rescale-children = K-bake) · law 9's +"never moves siblings" needs the free-context qualifier (in flow, a +union change legitimately reflows — Figma agrees) · min/max clamps +bound the box, never the envelope (declare). +Text: vertical alignment of clipped/overflowing content (T-E2 cites +math that doesn't exist) · auto-width basis in definite flex +(wraps-at-available vs Chromium's max-content — same deviation family +as the Taffy guards) · grow on the measured axis (spec example (c) is +currently a no-op and its annotation is wrong — fix the example) · +`max_lines: 0` semantics + max*lines→natural-size rule · font_size +domain (reject ≤0) · whitespace-only non-monotonicity · giant-word ink +escapes the world AABB (hit/cull miss — declare ink-bounds ⊃ box). +Transform: ~~flip×rotation composition order~~ (CLOSED E-A14: `T·R·F`, +innermost, tested) · flip on text (Figma mirrors; DEC-9 sub-decision, +uniform recommended) · flip on lens (outside-ops vs inside-ops) · +negative-determinant lens Matrix += second home for mirror (extend the E-A8 lint) · winding read source +(rotation reads the stored scalar — it \_is* intent; resolves the law-7 / +R-E2 / H5 three-way) · "spec-reserved" 3D ops need a declared v1 +runtime posture (render-skip + preserve ≠ silent drop) · singular +resolved matrices (CSS: not displayed — adopt?) · resolved-tier +overflow (finite doc → Inf via 2e38 sizes; N-2 guards writes only — +adopt Blink-style saturation?) · lens op origin's reference rect + +nested lens pre/post visibility · lens read tier (xywh = pre-ops box; +declare the post-ops envelope as a second named read) · NaN via lens op +params (op-layer guard must cover every op scalar). +Derived: empty-derived placement (box snaps to origin — declare +placeholder-at-pins) · empty group in flex spends two gaps (CSS agrees; +declare) · MM-6's "declared policy" for hidden children of derived +parents (write it: skipped from union). + +## Not built (the honest ⛔ ledger) + +`bool` (needs pathops), `vector` (E-A9 lock first), `image`/`embed`/ +`tray` payloads, ~~`flip_x/flip_y`~~ (BUILT — E-A14, 11 tests; only the +gesture policy DEC-9 is open), 3D lens ops (encoding reserved only), grid mode, +`max_lines`/`ellipsis` fields, percent pins, anchor-to-node (`wire`), +incremental invalidator, a pinned production text oracle, general font +fallback/bidi, complete source↔cluster/caret mapping, stable IR ids. Each is +_named_ in a.md §12, the REPORT lose column, or this census — nothing in this +list is silent. + +## Cost note (found by the census, engine-relevant) + +The lab's hug-chain measurement is exponential in nesting depth +(2^depth re-measures — each hug parent re-enters children twice) and +duplicates resolver reports on the re-entry. Fine for the lab's scenes; +phase 4's single-tree layout removes it structurally. Recorded so the +per-container architecture is never shipped as-is. diff --git a/archive/model-v2/anchor/MODEL.md b/archive/model-v2/anchor/MODEL.md new file mode 100644 index 00000000..c7c717b9 --- /dev/null +++ b/archive/model-v2/anchor/MODEL.md @@ -0,0 +1,181 @@ +# The anchor geometry model — consolidated statement + +Status: **proposal, post-experiments, pedantic-reviewed** (2026-07-07). +The model as it stands after E1–E7. Carries the _load-bearing_ +amendments inline (E-A1/2/3/9/10 and, by reference, E-A5); the full +amendment set and the review's lock conditions live in +[`REPORT.md`](./REPORT.md) and [`pedantic-review.md`](./pedantic-review.md). +Seed of the phase-3 normative rewrite; a.md remains the detailed draft +it supersedes clause by clause. + +## One sentence + +A node is an **anchored box with typed content**: the box comes from +intent, content is _realized into_ the box at render, and nothing +derivable is ever stored. + +## The two-axis core + +The E7 study crystallized what the kind table was reaching for. Every +kind answers two **independent** questions: + +1. **Where does the box come from?** — `declared` (intent) · + `measured` (content, one-way) · `derived` (children union) +2. **How does content fill the box?** — realization, at render, never + written back + +| kind | box | content realization | +| -------- | --------------------------------------------------------------- | ------------------------------------------------------------------ | +| `frame` | declared (Auto = hug) | children — bindings or flex | +| `shape` | declared | **parametric**: descriptor = f(box), then children in local space | +| `vector` | declared (Auto = reference bounds) | **mapped**: points in a reference rect, scaled `box/ref` at render | +| `text` | measured under intent constraints | **flowed**: re-wraps at the imposed box | +| `image` | declared | **fitted**: paint re-covers per fit mode | +| `embed` | declared w × measured h | flowed | +| `group` | derived (union of oriented children) | children in origin space | +| `bool` | derived (**op-result** bounds — D-5; subtract-to-empty ⇒ empty) | operands in origin space | +| `lens` | derived | children ∘ ops — **paint only** | + +`shape` here names the internal parametric payload family, not an authored +element. Draft 0 n0 XML lowers ``, ``, and `` into +this family; the frozen E3 TextIr dialect alone spells it +``. + +Why this shape is right, per the evidence: + +- _Parametric_ is Figma's own answer for primitives (star = count + + ratio; corner radius px-stable under non-uniform resize) and our + engine's current answer (`to_shape()` from size; fbs + `CanonicalLayerShape` size-free). XYWH-vs-shape dissolves: the box is + the shape's _input_, so layout and shape definition cannot fight. +- _Mapped_ is the Sketch/Figma/SVG convergence (normalized 0–1 points / + `normalizedSize` blob / viewBox): **resize never rewrites points** — + the blob stays the blob. Reference is a **rect** (observed non-zero + blob origins), not a bare size. Our io-figma already implements the + read side. +- _Flowed_ and _fitted_ are the standard measure-function and BoxFit + seams, already shipping. + +Child ownership is orthogonal to box source. A shape may own ordered, +free-positioned children in its declared local box: the primitive paints first, +then its descendants. Those descendants never measure or resize the shape and +never change its contribution to parent layout. Text therefore composes under a +shape as an ordinary child; there is no combined shape-with-text kind. + +## The laws + +1. **Intent only in the file.** Derivable ⇒ not encodable. The resolved + tier (boxes, matrices, bounds, materialized points, baked outlines) + is engine output. (Figma's file, notably, agrees on the intent side — + its normalized blob — while also shipping the bake; we ship only + intent.) +2. **No stored matrices outside `lens`.** Rotation is one scalar + (degrees), flips are two booleans (E-A2, corpus-forced), pivot is the + box center for boxed/measured kinds and the own-origin for derived + kinds. +3. **The box is the only negotiation surface with layout.** Realization + never feeds back into layout; measurement feeds forward only. + Acyclicity is _guarded_, not free: E-A5 makes End/Center/Span + bindings error-by-rule under parents with no resolvable extent + (derived boxes, Auto-hug axes) — remove that rule and Span ← hug ← + children is a real cycle. Locality: a leaf edit re-resolves up the + measure chain to the nearest fixed-extent ancestor, then its subtree + (18 µs per card subtree measured _under clean parent boxes_; + incremental invalidation itself is phase-4 work). +4. **Rotation is a post-layout paint transform** (DEC-0 second lock, + owner framing — supersedes the E1 default): sizing (flex + contributions, hug, derived unions) never reads rotation or flips; + the read tier (world AABBs, selection, hit) stays oriented. The + normative rule set incl. the V-4 group-box fork: + [`dec0-visual-only.md`](./dec0-visual-only.md). The E1 arm + (oriented-AABB participation) remains implemented and tested as the + documented alternative. + +5. **Styles are px-stable under resize** — strokes, radii, font sizes, + effects never scale with the box (non-scaling-stroke by + construction; every studied tool refuses SVG's default here). + The **two scales** are distinct operations, never a stored mode: + - **K / parameter scale** — an op-layer _bake_: multiplies style + scalars + sizes through the subtree (Grida already ships + `parametric_scale`; = Figma `rescale()`, Sketch K). + - **picture scale** — a stored `lens scale()` op: everything scales + at render, strokes and image fills included; the quarantined + SVG/Framer-Graphic semantics. No `` container is ever + _required_ — the default world is the layout world; the + proportional world is the opt-in lens. +6. **Points law** (E-A9): vector vertices live in the reference rect and + are written **only by vertex-editing gestures**. Resize, layout + stretch, grow — all are box writes; the mapping happens at render. + (Retires the current editor's per-resize vertex bake.) _Read side + research-grounded; the write side is **open until locked**: the ref + rect is free intent (exempt from law 1, like Figma's normalizedSize); + no renormalization as a gesture side effect; vertex-edit = an atomic + declared write-set with a stationarity guarantee; zero-extent ref + axis maps translation-only. See pedantic-review.md §B2._ +7. **Reads materialize, writes re-target.** `x/y/w/h/rotation` and + vertex coordinates always read from the resolved tier in real + coordinates (as Figma's Plugin API does over its normalized storage); + writes re-target the stored intent in delta form. The **text IR + materializes** — an agent always sees and writes real numbers, and + predicted geometry cold at 22/22 (E3 — n=3 models over 6 documents; + the probe protocol, not the score, is what phase 3 inherits: re-run + per grammar change). _Read half proven; the IR write half is open + pending stable ids (an agent editing-in-place has only positional + identity today)._ +8. **Strict states; writes in two declared regimes.** Invalid documents + are unrepresentable (unions, nullable boxes, no sentinels — + E2-proven encodable and byte-fixpoint). Writes are either + **re-targeted** (the coerce set: e.g. an x-write on an End pin + rewrites the offset; always reported, never silent) or **rejected + with a typed error** (the M-2 locked list: AxisOwnedBySpan, + OwnedByLayout, BoxDerived, InvalidNumber — a rejected op leaves the + document byte-identical, M-6). Which writes belong to which set is + enumerated spec, not implementer choice. +9. **Derived boxes: bindings place the origin, never the union** + (E-A1) — child edits change a group's _reported_ box but never move + siblings in world space. Reported box = origin + union. + +## Resolution — one pure function + +`resolve(document, fonts, resources, viewport) → Resolved`, four phases: +**measure** (bottom-up naturals) → **layout** (flex over contributions / +bindings; contributions abstract over rotation AABBs, union boxes, +mapped bounds — the accidental plugin seam of E6) → **transform** +(`from_box_center`; origin pivot for derived; lens ops appended) → +**bounds** (oriented corners → world AABBs). Measured floor: 10k nodes +in 5.4 ms unoptimized, linear; subtree resolve cost bounded at 18 µs +(clean-parent precondition — see law 3); the incremental invalidator +itself is phase-4 work. + +## The lifecycle, concretely + +A flex card list; a card holds an icon (`vector`) and a title (`text`): + +- **stretch the card** (cross-axis fill): one intent state + (`self_align: stretch`); at render the icon's points re-map to the + new box, the title re-wraps, the image fill re-covers. Document + writes: **zero** (layout did it). +- **resize the icon by hand**: two field writes (w, h). Points + untouched. +- **rotate the card**: one field write; paint-only — the list does NOT reflow (DEC-0), measured + smooth; a spinning-motion variant targets a lens channel instead. +- **K-scale the card**: sanctioned bake — sizes, radii, stroke widths, + font size multiply; still no matrix anywhere. +- **edit an icon vertex**: vertex write in reference space; the icon's + Auto box re-derives; bindings re-resolve. Sibling cards never move. + +## What this retires / what stays open + +Retired: per-node transforms, position enums, per-resize vertex bakes, +`MIN_SIZE`-style sentinels, the ICB special regime, the mandatory +graphics container. Open (unchanged from a.md §12 + run findings): +`bool` operand semantics, grid mode, attributed text runs, percent pins, +anchor-to-node (`wire`), M-4 RMW policy, stable ids in the text IR. + +## Suggested next step + +Phase-3 proper: rewrite `models/anchor.md` against this statement as the +normative spec (every conformance `POL` locked, E-A1…E-A10 folded, +applicability matrix regenerated with `flip_x/flip_y` and the vector +reference rect), regenerate the fbs draft (header + `VectorPayload +{ ref: Rect, network }`), then WG graduation. diff --git a/archive/model-v2/anchor/README.md b/archive/model-v2/anchor/README.md new file mode 100644 index 00000000..ba3e2ac1 --- /dev/null +++ b/archive/model-v2/anchor/README.md @@ -0,0 +1,143 @@ +# `anchor` — phase-3 kick-off & experiments workbench + +This directory hosts the experiments, spikes, and spec drafts for +**`anchor`**, the winning model of the model-v2 redesign +(decided 2026-07-07 via the owner triage). This README is written to be +**self-sufficient for session re-entry after context compaction** — read it +top to bottom and you have working state. + +> **2026-07-07 — the ledger has been RUN.** E1–E7 and E10 are complete with +> verdicts; the lab implementation now lives at +> [`crates/n0-model`](../../crates/n0-model) (promoted from `lab/` at the +> landing of gridaco/nothing#5), with its full conformance-derived suite +> green. Start at **[`REPORT.md`](./REPORT.md)** — win/lose/lessons-learnt +> and the fourteen spec deltas (E-A1…E-A14 + Taffy guards). The +> consolidated model statement — the phase-3 seed — is +> **[`MODEL.md`](./MODEL.md)**. Frozen papers below still say `lab/`, +> `../engine`, and `spike-canvas/` — read those as the pre-promotion +> layout (map: `lab/` → `crates/n0-model`, `../engine` → `crates/n0`, +> `spike-canvas/` → `crates/n0_dev`). This directory itself was `a/` +> before landing, and the experiment dirs carried `eN-` ledger +> prefixes (`e1-rotation-in-flow` → `rotation-in-flow/`, `e2-format` → +> `format-roundtrip/`, `e3-text-ir` → `text-ir/`, `e4-resolver` → +> `resolver-spike/`, `e5-svg-corpus` → `svg-corpus/`, +> `e6-pluggable-layout` → `pluggable-layout/`, `e7-shape-points-scale` +> → `shape-points-scale/`); the E-ids remain the ledger's register keys +> in the table below and throughout the frozen prose. + +## State of decisions (compressed) + +| decision | state | where | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| Main model | **`anchor`** — intent-canonical anchored box model | [`../models/anchor.md`](../models/anchor.md) (the model spec draft) | +| How decided | 32-question model-blind owner triage; deciders: canvas-truth (#1) + "the CSS path already exists as the `htmlcss` engine; the new taxonomy exists to be a new editor-first standard" (#6) | [`../triage.md`](../triage.md), instrument: [`../survey.md`](../survey.md) | +| Retired | `sheet` (adopt-CSS — its domain is the existing htmlcss engine), `bake` (post-T1), `wire` (deferred; re-enters additively via `Pin.to`) | [`../finale.md`](../finale.md), [`../models/`](../models/) | +| ~~Open fork~~ | rotation-in-flow — **DECIDED: VISUAL-ONLY** (DEC-0 second lock, 2026-07-07; rules in [`dec0-visual-only.md`](./dec0-visual-only.md)); E1's layout-visible arm stays implemented + tested as the documented alternative | [`rotation-in-flow/verdict.md`](./rotation-in-flow/verdict.md) | + +### The five triage amendments (binding on everything here) + +1. **No file-carried switch-memory** — toggle-back restoration is editor + session state; the file holds only current truth. +2. **Strict states, lenient writes** — invalid documents unrepresentable; + the write/op layer never hard-walls (coerce/redirect, always reported, + never silent). H12 applies at the op layer as + "effective-or-coerced-with-report". +3. **Agent text IR is a first-class surface** — an XML-ish textual + projection that round-trips with the binary format; audience = LLMs + (must be able to write it and _predict geometry mentally_). H1's + audience is agents, not human file-readers. +4. **Familiar vocabulary** — `x`, `width`, `rotation`, `flex`, `gap`, + `padding` wherever semantics coincide; new terms only where the + correction is the point. +5. **SVG import ≈100%** — hard requirement; exotic-transform carrying + mechanism open (lens-quarantine default, declared degradation + acceptable), decided by corpus measurement (E5). + +## The model in ten lines (recap of `../models/anchor.md`) + +- Node = **uniform header + typed payload**. Header: `x`/`y` as + `AxisBinding = Pin{start|center|end, offset} | Span{start, end}`; + `width`/`height` as `SizeIntent = Fixed | Auto` (no Fill — growth via + `grow`, stretch via `self_align`/`Span`); `rotation: f32` degrees; + `flow`, `grow`, `self_align`; layer props. No stored matrices anywhere. +- Kinds: `frame, tray, shape, image, text, embed, vector, group, bool, +lens`. Shape descriptors are size-free, evaluated at the resolved box. +- Box sources: declared / measured (text, vector — intent = constraints) / + derived (group, bool, lens — never store size). +- Rotation pivot: box center (boxed/measured); own origin + gesture + compensation (derived). `lens` = sole home for skew/matrix/3D ops. +- Resolution: measure → layout (flex over unrotated boxes — DEC-0 + visual-only; rotation applies in phase T, post-layout) → + transforms (`from_box_center`) → bounds. One-way; resolved tier never + serializes ("derivable ⇒ not encodable"). + +## Experiment ledger + +One subdirectory per experiment (`e1-…/`), each with its own README +(question, method, decides-what) and a `verdict.md` when concluded. +Code: small standalone crates/scripts inside the experiment dir, **not** +wired into the workspace; promote into `crates/`/`format/` only at phase 4. + +| id | experiment | status (2026-07-07) | +| --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **E1** | rotation-in-flow prototype | **DONE — layout-visible locked** (0 overlap vs 1,830 px²; smooth within analytic bound; breathing confirms two-lane) → [verdict](./rotation-in-flow/verdict.md), [demo.html](./rotation-in-flow/demo.html) | +| **E2** | fbs schema draft + codec round-trip | **DONE — pass** (S-1 byte-fixpoint, H11 structural unset witnessed, M-4 both directions; RMW policy owed) → [verdict](./format-roundtrip/README.md) | +| **E3** | agent text IR | **DONE — validated** (frontier 22/22 cold prediction ×2; grammar + probes + scorer reusable) → [verdict](./text-ir/verdict.md) | +| **E4** | resolver spike | **DONE — viable** (10k/5.4 ms; linear; 18 µs locality bound; 2 Taffy findings) → [verdict](./resolver-spike/verdict.md) | +| **E5** | SVG import corpus measurement | **DONE — quarantine + native-flip amendment** (1M transforms: shear 0.95%, flip 26.1%) → [verdict](./svg-corpus/verdict.md) | +| **E6** | pluggable-layout sketch | **DONE — non-binding** (the contributions→slots seam exists; keep core at two modes) → [sketch](./pluggable-layout/SKETCH.md) | +| **E7** | shape-vs-layout / points / two scales (owner question) | **DONE — research verdict** (XYWH-vs-shape solved by design; E-A9 vector reference-space; E-A10 two-scales rule; Graphics container rejected-as-requirement) → [findings](./shape-points-scale/README.md) | +| **E-rev** | pedantic panel (bedrock/UX/eng/market) + edge sweep | **DONE** — core survives; 7 MODEL.md corrections applied; E-A11/E-A12 minted (grow/stretch × rotation pops, machine-verified); E8 (CSS projection) + E9 (Figma corpus) opened → [pedantic-review.md](./pedantic-review.md), [edge-cases/](./edge-cases) | +| **GROUP** | what a group is (owner question) | **DONE** — definition locked (“a named set with a coordinate space — nothing else”); E-A13 proposed (constraint pass-through, acyclic via E-A1); Figma semantics verified from primary sources → [`GROUP.md`](./GROUP.md) | +| **E-census** | edge census across all kinds + import maps | **DONE** — [`LIMITS.md`](./LIMITS.md) (kind×mechanism matrix, 5 bugs found+fixed, 5 blockers, the 🔶 punch list); [`COMPAT.md`](./COMPAT.md) (css/svg/figma → grida maps + harness clause H13); lab at **81 tests** (92 after the flip arm) | +| **E-flip** | cross-zero resize + flip semantics (owner red flag) | **BUILT** — E-A14: flip live in the lab (pivot per kind, `T·R·F` innermost, layout-invisible, σ-exact ungroup bake, NegativeExtent wall, `resize_drag` out-and-back identity); 11 tests; three-policy demo scene in [`edge-cases/`](./edge-cases); gesture policy = DEC-9 | +| **DECISIONS** | the open owner-call register | **DEC-0 DECIDED: visual-only (owner framing; second lock)** — [`DECISIONS.md`](./DECISIONS.md) + [`dec0-visual-only.md`](./dec0-visual-only.md); DEC-1/2/3 closed n/a by it · DEC-4 text determinism · DEC-5 f32 budget · DEC-6 bool bounds · DEC-7 format RMW · DEC-8 E8/E9 · DEC-9 wall/slide/flip · DEC-10 lens naming — answer in one pass; adopted-by-evidence appendix carries the veto window | +| **DEC-0 fork demo** | see the framing fork, not guess it | **BUILT** — [`dec0-fork/`](./dec0-fork): seven scenes × two framings side by side (`lab/src/bin/fork.rs`, real resolver both arms), live overlap / container-breathing / ink-escape meters; the two-lane asymmetry scene (header-rot vs lens-rot) | +| **DEC-0 flip** | the second lock: visual-only default (CSS framing, CSS-pure sizing) | **DONE** — [`dec0-visual-only.md`](./dec0-visual-only.md) (rules V-1…V-10; the V-4 group-box fork was real UB, decided sizing-tier); lab default flipped, hug/union CSS-pure behind the flag; `tests/visual_only.rs` (+14, suite **114**); E-A4/7/8/11/12 retired; spike re-cut (ink-bounds chrome, shots regenerated) | +| **free-editing demo** | the base case, hands on (owner question: "without layout, how does it work?") | **BUILT** — [`free-editing/`](./free-editing): live free-context mini-editor (JS mirror of the free rules — no Taffy, which is the point); live canonical IR, per-gesture write counts incl. typed errors, 0-write artboard responsiveness | +| **E10** | the feel spike: does the model drive a REAL editor? (owner: feel it + textbook) | **BUILT** — [`spike-canvas/`](./spike-canvas): native winit+Skia app on the lab (`cargo run --release`); resolve-per-frame thesis MEASURED (starter frame 0.17 ms; 10k nodes paint-bound at 9 ms); arena+SOA storage evolution in the lab (**up to 11.5× resolver speedup**, 100 tests); interaction FSM, HUD w/ E-A7 readout, cross-zero flip gesture, undo, live+editable IR panel, reports-as-badges; [`TEXTBOOK.md`](../../crates/n0_dev/TEXTBOOK.md) + [`spike-readout.md`](./spike-readout.md) | +| **ENGINE** | the phase-4 layer programs (owner: browser/game-engine techniques from day 1) | **BUILT 2026-07-08** — [`ENGINE.md`](./ENGINE.md) (contracts ENG-0…ENG-5 + SETUP S-1…S-7, every claim validity-tagged) + a **day-1 skeleton** at [`engine/`](../engine) (`anchor-engine`): `resolve → drawlist → paint` pipeline, spatial `query`, `journal`/`replay`, `damage`, generational `ident`, `oracle` tags, `trace`; the spike is **re-hosted** onto it and the gate is green — 4 shots byte-identical, 3 replays deterministic, resolve/build budgets baselined ([`engine/bin/gate.rs`](../engine/src/bin/gate.rs)). Lab grew the model-crate seams (`Op`/`apply`/`DirtyClass`, generations, opt accessors, `serde`); the then-current suite was green | +| **ANIMATION** | explicit-time SVG animation through the proving engine | **BOUNDED CHECKPOINT 2026-07-14** — [`engine/ANIMATION.md`](../engine/ANIMATION.md): cumulative SVG Profiles 0–6 lower scalar/color/transform effects plus smooth and discrete path geometry into the existing effective-value/frame/cache path; exact-time offline rendering, a caller-owned playback clock, and a controlled native host are implemented. General SVG materialization, arc morphing, native n0 XML animation, broader animated paint families, a compositor/product runtime, and production migration remain pending | + +All verdicts + the spec deltas roll up in [`REPORT.md`](./REPORT.md). +Implementation: [`crates/n0-model`](../../crates/n0-model) (promoted from +`lab/`) — `cargo test -p n0-model`; the arg-driven bins `e4` and `e5scan` +survive, while the one-shot harnesses `e1`, `e3`, `edge`, `fork` were +retired at promotion (their outputs and captured essence are frozen in +this archive). The engine is [`crates/n0`](../../crates/n0) (promoted +from `../engine`, `anchor-engine`), the spike re-hosted onto it as +[`crates/n0_dev`](../../crates/n0_dev) — see [`ENGINE.md`](./ENGINE.md) +and the current [`ANIMATION.md`](../../crates/n0/ANIMATION.md) +checkpoint. + +## Phase-3 definition of done + +1. DEC-0 verdict folded in (visual-only, rules V-1…V-10) → R-3/OP-ROT-2 + become `INV`, not `POL`. +2. Normative spec: `../models/anchor.md` rewritten as spec (amendments folded, + every `POL` in [`../conformance.md`](../conformance.md) answered and + locked, applicability matrix final). +3. `grida.fbs` draft (E2) + text-IR grammar (E3) as spec appendices. +4. Conformance corpus stubbed with stable IDs (`covered_by` linkage). +5. WG graduation: code-agnostic write-up into `docs/wg/` (docs-wg + doctrine), with model-v2 remaining the working record. + +## Re-entry protocol (post-compaction) + +Read in this order — ~10 minutes to full context: + +1. This file (state + amendments + ledger). +2. [`../models/anchor.md`](../models/anchor.md) — the model itself. +3. [`../triage.md`](../triage.md) — why, in the owner's own words. +4. [`../conformance.md`](../conformance.md) §FORK rows + + [`../editor.md`](../editor.md) doctrine — the behavioral contracts. +5. Skim [`../problems.md`](../problems.md) / [`../harnesses.md`](../harnesses.md) + only when a design argument needs its source. + +Also standing: this workbench lives on the **`model-v2-anchor`** branch +(tracking issue +[gridaco/grida#957](https://github.com/gridaco/grida/issues/957), pinned); +pushes require owner approval, per turn. The session convention is +grounding-first, problems before solutions, and no scope beyond +**Rust engine + format spec** — other seams (TS editor, WASM bindings) +follow after the model lands. diff --git a/archive/model-v2/anchor/REPORT.md b/archive/model-v2/anchor/REPORT.md new file mode 100644 index 00000000..49181c06 --- /dev/null +++ b/archive/model-v2/anchor/REPORT.md @@ -0,0 +1,214 @@ +# REPORT — prove `anchor` + +Autonomous run, 2026-07-07. Goal: **prove Model A** (`anchor`, +[`../models/anchor.md`](../models/anchor.md)) by building it and running the +experiment ledger ([`README.md`](./README.md)), ending in +win / lose / lessons-learnt. + +## Verdict: WIN — with five earned amendments and an honest loss column + +The model was implemented end-to-end in a standalone lab crate +([`lab/`](./lab), ~2,300 lines incl. tests, not wired into the repo +workspace), and every core claim of the spec survived contact with +execution — but not unchanged: implementation forced five concrete spec +corrections that document review had not found. That is what "proven" +means here: the model works, and we now know _where its paper version +was wrong_. + +## Scoreboard + +| exp | question | verdict | +| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Lab core** | does the model implement? | **56/56 conformance-derived tests green** — MM-1/2/3/4/5/6/7/9, G-1/2/4/5/E1/E2/E3, R-1/2/3(both arms)/4/5/E1, L-3/4/5/7/E1/E3, T-1, D-1/2/3/6/E1, lens transparency, ops write-counts, typed errors, M-6, NaN boundary, IR round-trip | +| **E1** rotation-in-flow | AABB-participates vs visual-only | **DECIDED: layout-visible** — 0 px² overlap at every angle vs 1,830 px² in the CSS arm; displacement smooth (≤3.45 px/2°, inside the √(w²+h²) analytic bound); rotate stays a 1-field write; container breathing (56.6 px full-spin) _confirms_ the two-lane motion rule instead of weakening the decision. R-3/OP-ROT-2 → INV. [verdict](./rotation-in-flow/verdict.md), [demo.html](./rotation-in-flow/demo.html) | +| **E2** format | encodes under fbs header rules? | **PASS** — schema compiles; JSON→bin→JSON→bin **byte-identical** (S-1); structural unset witnessed in output (H11); additive evolution both directions (M-4). One policy owed: RMW-preservation of unknown fields. [verdict](./format-roundtrip/README.md) | +| **E3** agent text IR | can an LLM predict geometry cold? | **VALIDATED** — two frontier agents **22/22 (100%)** from a 150-line grammar alone, including wrap arithmetic, grow, hug, rotated-in-flow, group origin rule; small-model control 17/22 with slip-class errors only. [verdict](./text-ir/verdict.md) | +| **E4** resolver spike | architecture viable? | **VIABLE** — 10k free nodes / 5.4 ms, all-flex worst case 237 nodes/ms linear, 18 µs subtree locality bound; replaces the 26-arm branch forest + `atan2` reconstruction + `MIN_SIZE_DIRTY_HACK` with ~700 uniform lines. Two Taffy dependency findings. [verdict](./resolver-spike/verdict.md) | +| **E5** SVG corpus | quarantine vs degrade? | **MEASURED: quarantine + one amendment** — 1,003,787 transforms / 7,138 files: true shear is 0.95% (lens is right); **flip is 26.1% → must be native** (`flip_x`/`flip_y` header bools); paint transforms (39%) never touch node geometry. [verdict](./svg-corpus/verdict.md) | +| **E6** pluggable layout | the #28 curiosity | sketched, non-binding: the `(contributions → slots)` seam already exists because E1's rotated AABBs forced it; keep core at two modes, E3 is the veto. [sketch](./pluggable-layout/SKETCH.md) | + +## What implementation forced into the spec (the run's real yield) + +These are the deltas `models/anchor.md` must absorb in its phase-3 rewrite — +each one was _discovered by a failing test or a measurement_, not by +argument: + +1. **E-A1 — derived-box bindings place the origin, not the union box.** + The naive reading of §2.1 (bindings position the box) makes D-2 fail: + moving child A shifts the union, which re-places the box, which moves + sibling B in world space — the exact P6 instability the model exists + to kill. §8's phrase "places the _space_" must be promoted to a + normative rule: pins bind the group-space origin; the reported box is + `origin + union`; reads report the box; writes re-target by delta. + (Found by `d2_sibling_stability_under_rotated_group` failing.) +2. **E-A2 — native flip.** Two header booleans (`flip_x`/`flip_y`), + center-applied. Measured justification: 159,872 single-axis mirrors + in the wild corpus — a "quarantine" firing on 24% of files is not a + quarantine. Answers R-E5. (E5) +3. **E-A3 — the two stretches are different rules and must be headlined:** + container `cross:"stretch"` affects only auto-cross children; child + `align:"stretch"` means _fill_ and overrides Fixed (it is the format's + only cross-axis fill, per §2.2's no-Fill design). The first grammar + draft under-specified this and the truth run caught it. (E3) +4. **E-A4 — grow × rotation declared:** grow expands the _slot_; a + rotated child's box keeps its basis, centered in the grown slot. + (Lab policy that needs spec text; found writing the flex path.) +5. **E-A5 — underdetermined-binding rules:** End/Center/Span pins under + a derived-box parent or an Auto-hug free frame are `error-by-rule` + (reported, resolved as Start) — the spec's applicability matrix has + no row for these; now it needs one. +6. **E-A6 — M-4 needs a policy, not a hope:** FlatBuffers read-skips + unknown fields but decode→re-encode through an old schema _drops_ + them (verified). Choose: buffer-retention patching, or writers + version-gated to newest schema. (E2) +7. **E-A7 — editor note:** the rotated envelope peaks at + `θ* = atan(h/w)`, not 90° — HUD affordances should surface the + envelope or mid-turn width reads as a bug. (E1) +8. **E-A8 — the lens-only-rotate lint must be context-scoped** (owner + question, post-run): a.md §3.3 flags a lens containing only `Rotate` + as "use header rotation" — but under the E1-locked semantics, + lens-rotate **in flow** is the legitimate, structurally-distinct + escape to paint-only rotation (proven: `lens_rotate_is_the_visual_only_twin` + reproduces the CSS arm exactly under the default flag). The lint + applies only in _free_ context, where header rotation is equivalent. +9. **E-A9 — vector adopts the reference-space mapping** (owner question + → E7 research, 2026-07-07): `vector` changes from "measured box, no + size intent" to points-in-reference-space + ordinary size intent, + mapped by `resolved_size / reference_size` at render — the structure + Sketch (normalized 0–1 points, no transform in format) and Figma + (`normalizedSize`) both converged on. Kills the current editor's + per-resize vertex bake (verified outlier), defines vectors under + grow/stretch, keeps resize a 1–2 field write. Refinement from our own + io-figma notes: the reference space is a **rect, not a size** + (observed non-zero blob origins) — store a reference rect or + normalize origin at write. Reference implementation already in-repo: + `scaleVectorNetworkFromNormalizedSize` (io-figma lib.ts) + the + fixture test proving Figma ships `normalizedSize != size` nodes. + See [`shape-points-scale/`](./shape-points-scale/README.md). +10. **E-A10 — the two-scales rule, locked** (E7): plain resize = + geometry re-evaluation with px-stable styles (non-scaling-stroke by + construction); parameter scale (K) stays an op-layer bake (Grida + already ships `parametric_scale`, = Figma `rescale()`); retained + picture-scale (strokes + image fills scale) exists only as a lens + `Scale` op. Image fills always re-fit at the resolved box per fit + mode — never baked, never rasterized with the shape. The + Framer/Sketch `` container is rejected as a requirement + (evidence: its mandatoriness is their documented pain), kept as the + lens escape. +11. **E-A11 — grow × rotation continuity** (pedantic pass + edge sweep, + 2026-07-07): E-A4 as declared is discontinuous — a `grow:1` card + snaps from filled (300 px) to basis (60 px) at the first degree of + rotation (measured 240 px/3° jump, `edge-cases/` scene `grow`). + Fix direction: inverse-envelope fill (box takes the largest extent + whose rotated envelope fits the grown slot — continuous at 0°). +12. **E-A12 — stretch/re-measure × rotation declared** (same pass): + the lab silently skips stretch and text re-measure for rotated + in-flow children — the unstated rule that makes "zero overlap" and + "single-pass" true. Promote to spec text (ignored-by-rule **with + report** for θ≠0), same continuity treatment as E-A11 (measured + 170→100 px pop, scene `stretch`). +13. **E-A13 — group constraint transparency** (owner question + verified + Figma research, 2026-07-07): binding reference = nearest non-derived + ancestor (Figma-parity pass-through), acyclic _because of_ E-A1's + stored origins — no re-fit writes needed; AL child-props do NOT pass + through (verified Figma parity); v1 restricts non-Start pins to + unrotated derived chains. Full chapter: [`GROUP.md`](./GROUP.md). +14. **E-A14 — flip semantics built** (owner-raised cross-zero red flag, + 2026-07-07): the four evidence-forced facts — pivot per kind exactly as + rotation (pedantic B1, now lab-tested: D-2 holds under a flipped + group); composition **innermost** (`T·R·F`, mirror first, then turn); + flip is **layout-invisible by construction** for boxed kinds + (center-applied ⇒ AABB unchanged — the one transform that never pops + layout); extents stay non-negative bedrock (`set_width(−50)` = + typed `NegativeExtent`, doc untouched) while the DRAG re-targets + (|extent| + flip toggle + re-pin, out-and-back = document identity). + Ungroup bakes flips exactly via mirror conjugation + (`F·R(θ) = R(σθ)·F`, σ = −1 iff single-axis). 11 tests + ([`lab/tests/flip.rs`](./lab/tests/flip.rs)); which gesture POLICY + ships (wall/slide/flip) is open — [`DECISIONS.md`](./DECISIONS.md) + DEC-9, demo'd in [`edge-cases/`](./edge-cases). +15. **Pedantic review (2026-07-07)** — full findings, lock conditions, + and two new ledger experiments (**E8** CSS-projection measurement; + **E9** Figma-corpus scan): [`pedantic-review.md`](./pedantic-review.md). + MODEL.md surgically corrected same day (bool cell, law 8 two + regimes, E-A5 acyclicity guard stated, locality qualified, E3 + caveat, law 6 marked open-pending-lock). +16. **DEC-0 second lock — the default is VISUAL-ONLY (owner framing, + 2026-07-07):** rotation is a post-layout paint transform; sizing is + CSS-pure (never reads rotation/flips — flex, hug, derived unions); + reads stay oriented. Supersedes E1's default; the E1 arm remains + implemented + tested as the documented alternative. The gated spec + review found and closed one real UB (the V-4 group-box fork — a + derived box's four consumers could read two different values; + decided: sizing-tier union). Normative rules: + [`dec0-visual-only.md`](./dec0-visual-only.md); conformance: + `lab/tests/visual_only.rs` (suite 114). Retires E-A4/E-A7/E-A8/ + E-A11/E-A12; closes DEC-1/2/3 n/a. +17. **Dependency guards (phase 4):** Taffy rounds unless disabled (L-7); + Taffy's intrinsic pass inflates growable-item contributions by the + container's own padding, deviating from L-3 _and_ Chromium — guard by + stripping grow in indefinite-main intrinsic runs, and pin L-3's + conformance test to Chromium, never to the layout library. (E4) + +## Lose column — what this run did not prove + +- **Payload coverage**: `tray`, `image`, `embed`, `vector`, `bool` were + not implemented (no new geometry mechanism among them — but "no new + mechanism" is itself an unproven claim for `bool`'s op-result box). +- **Wrap** got minimal exercise; **grid** none (additive future by + design, untested by construction). +- **CRDT**: C-1/C-5 were demonstrated as field-level merges in tests, + but no real replicated backend ran the C-matrix. +- **No incremental resolver**: locality was _bounded_ (18 µs/subtree), + not implemented; N-4's invalidation claim rests on phase-order + structure plus that bound. +- **"Feel" by proxy**: E1 measured overlap/continuity/breathing and + ships an interactive demo, but no human ran the scrubber yet — the + owner should open `rotation-in-flow/demo.html` and disagree if the + numbers lied. +- **E3 n=3**: two frontier + one small model, six documents. Convincing + signal, small sample; the probe protocol is repeatable on demand. +- **Text IR carries no node ids** — authoring round-trips; _editing_ + identity does not yet (recorded in the E3 verdict). +- The lab's per-container Taffy runs and hug double-pass mean E4's flex + numbers are a floor, not an engine benchmark. + +## Lessons learnt + +1. **Building found what reviewing could not.** Five of the eight spec + deltas came from failing tests or corpus counts, none from re-reading + the documents. The triage's "challenge the ideal empirically" posture + (#5) was the single highest-yield decision of the whole model-v2 + effort. +2. **The best correction came from the model's own test.** D-2 + (sibling stability) is _in the conformance corpus because the paper + analysis said groups were solved_. The test disagreed with the + implementation of the paper's own words — "places the space" turned + out to be load-bearing prose nobody had cashed out. Write the test + corpus before believing the prose. +3. **Dependencies are spec surface.** Two Taffy behaviors (rounding, + intrinsic grow inflation) would have silently become "the model" had + the lab pinned tests to the library instead of to declared rules. + Conformance oracles must outrank implementation convenience. +4. **Sentinel-freedom is cheap at birth, expensive to retrofit** — the + whole H11 discipline cost one afternoon in a fresh schema (flatc + proved unset-vs-default structurally in the decoded JSON), versus the + `max_lines: 0` / `(0,0)-aspect` hacks the current format carries. +5. **LLM-predictability works as a design _instrument_, not just a + requirement**: prediction failures localize spec ambiguity (the + small model's slot-center slip and the stretch gap were both wording + defects surfaced as wrong numbers). Re-run the E3 probe after every + grammar change; treat sub-100% frontier scores as spec bugs. +6. **Measure the corpus before designing the escape hatch.** The lens + was designed for skew; the corpus said the real problem was flips — + a class nobody had ranked. One scanner afternoon re-ranked the + priorities of the capability model. + +## What's next (phase 3, unchanged in shape, now unblocked) + +Rewrite `models/anchor.md` as the normative spec with E-A1…E-A7 folded in and +every conformance `POL` locked (R-3 now INV); graduate the WG write-up +per docs-wg doctrine; promote `anchor.fbs` + the text-IR grammar to spec +appendices; then phase 4 against the real engine with the two Taffy +guards. Nothing in this run is committed to git — the whole workbench +remains untracked working state for the owner to review. diff --git a/archive/model-v2/anchor/dec0-fork/fork.json b/archive/model-v2/anchor/dec0-fork/fork.json new file mode 100644 index 00000000..173a8b3b --- /dev/null +++ b/archive/model-v2/anchor/dec0-fork/fork.json @@ -0,0 +1,6468 @@ +{ + "step": 3, + "scenes": [ + { + "id": "grow", + "name": "grow x rotation", + "labels": ["container", "a", "grow+rot", "c"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 55.0, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 222.66, 53.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 225.39, 52.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 228.19, 50.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 231.05, 49.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 233.96, 48.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 236.92, 48.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 239.91, 47.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 242.93, 47.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 245.97, 46.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 249.02, 46.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 252.07, 46.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 255.12, 46.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 258.15, 47.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 261.16, 47.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 264.14, 48.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 267.08, 49.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 269.98, 50.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 272.82, 51.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 275.59, 52.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 278.3, 54.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 280.93, 55.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 283.48, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 285.93, 59.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 288.28, 61.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 290.53, 63.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 292.67, 65.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 294.69, 67.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 296.59, 69.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 298.36, 72.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 300.0, 75.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 301.5, 77.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 302.86, 80.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 304.08, 83.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 305.14, 86.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 306.06, 88.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 306.82, 91.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 307.43, 94.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 307.88, 97.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 308.17, 100.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 308.3, 104.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 308.27, 107.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 308.08, 110.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 307.74, 113.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 307.23, 116.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 306.57, 119.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 305.75, 122.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 304.78, 124.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 303.66, 127.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 302.39, 130.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 300.98, 133.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 299.43, 135.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 297.74, 138.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 295.93, 140.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 293.98, 143.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 291.92, 145.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 289.74, 147.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 287.45, 149.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 285.06, 151.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 282.58, 153.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 280.0, 155.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 55.0, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 102.82, 47.22, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 106.05, 39.59, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 109.67, 32.15, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 113.67, 24.91, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 118.05, 17.88, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 122.79, 11.09, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 127.88, 4.57, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 133.31, -1.69, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 139.05, -7.65, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 145.1, -13.3, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 151.43, -18.63, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 158.04, -23.62, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 164.89, -28.26, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 171.98, -32.53, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 179.29, -36.42, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 186.79, -39.93, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 194.46, -43.04, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 202.28, -45.74, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 210.24, -48.03, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 218.3, -49.9, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 226.45, -51.35, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 234.67, -52.37, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 242.92, -52.96, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 251.2, -53.11, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 259.47, -52.83, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 267.72, -52.12, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 275.92, -50.97, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 284.05, -49.4, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 292.08, -47.41, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 300.0, -45.0, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 307.78, -42.18, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 315.41, -38.95, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 322.85, -35.33, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 330.09, -31.33, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 337.12, -26.95, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 343.91, -22.21, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 350.43, -17.12, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 356.69, -11.69, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 362.65, -5.95, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 368.3, 0.1, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 373.63, 6.43, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 378.62, 13.04, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 383.26, 19.89, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 387.53, 26.98, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 391.42, 34.29, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 394.93, 41.79, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 398.04, 49.46, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 400.74, 57.28, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 403.03, 65.24, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 404.9, 73.3, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 406.35, 81.45, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 407.37, 89.67, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 407.96, 97.92, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 408.11, 106.2, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 407.83, 114.47, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 407.12, 122.72, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 405.97, 130.92, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 404.4, 139.05, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 402.41, 147.08, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 400.0, 155.0, 300.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 2.1], + [0.0, 460.0, 170.0, 8.9], + [0.0, 460.0, 170.0, 15.4], + [0.0, 460.0, 170.0, 21.7], + [0.0, 460.0, 170.0, 27.6], + [0.0, 460.0, 170.0, 33.3], + [0.0, 460.0, 170.0, 38.6], + [0.0, 460.0, 170.0, 43.6], + [0.0, 460.0, 170.0, 48.3], + [0.0, 460.0, 170.0, 52.5], + [0.0, 460.0, 170.0, 56.4], + [0.0, 460.0, 170.0, 59.9], + [0.0, 460.0, 170.0, 63.0], + [0.0, 460.0, 170.0, 65.7], + [0.0, 460.0, 170.0, 68.0], + [0.0, 460.0, 170.0, 69.9], + [0.0, 460.0, 170.0, 71.4], + [0.0, 460.0, 170.0, 72.4], + [0.0, 460.0, 170.0, 73.0], + [0.0, 460.0, 170.0, 73.1], + [0.0, 460.0, 170.0, 72.8], + [0.0, 460.0, 170.0, 72.1], + [0.0, 460.0, 170.0, 71.0], + [0.0, 460.0, 170.0, 69.4], + [0.0, 460.0, 170.0, 67.4], + [0.0, 460.0, 170.0, 65.0], + [0.0, 460.0, 170.0, 67.4], + [0.0, 460.0, 170.0, 69.4], + [0.0, 460.0, 170.0, 71.0], + [0.0, 460.0, 170.0, 72.1], + [0.0, 460.0, 170.0, 72.8], + [0.0, 460.0, 170.0, 73.1], + [0.0, 460.0, 170.0, 73.0], + [0.0, 460.0, 170.0, 72.4], + [0.0, 460.0, 170.0, 71.4], + [0.0, 460.0, 170.0, 69.9], + [0.0, 460.0, 170.0, 68.0], + [0.0, 460.0, 170.0, 65.7], + [0.0, 460.0, 170.0, 63.0], + [0.0, 460.0, 170.0, 59.9], + [0.0, 460.0, 170.0, 56.4], + [0.0, 460.0, 170.0, 52.5], + [0.0, 460.0, 170.0, 48.3], + [0.0, 460.0, 170.0, 43.6], + [0.0, 460.0, 170.0, 38.6], + [0.0, 460.0, 170.0, 33.3], + [0.0, 460.0, 170.0, 27.6], + [0.0, 460.0, 170.0, 21.7], + [0.0, 460.0, 170.0, 15.4], + [0.0, 460.0, 170.0, 8.9], + [0.0, 460.0, 170.0, 2.1], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0] + ] + } + }, + { + "id": "stretch", + "name": "stretch x rotation", + "labels": ["container", "a", "stretch+rot", "c"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 30.0, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 105.23, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 175.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 110.45, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 180.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 115.64, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 184.9, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 120.79, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 189.48, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 125.88, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 193.84, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 130.9, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 197.97, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 135.84, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 201.85, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 140.67, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 205.49, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 145.4, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 208.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 150.0, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 211.96, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 154.46, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 214.78, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 158.78, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 217.32, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 162.93, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 219.56, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 166.91, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 221.5, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 170.71, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.14, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 174.31, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.46, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 177.71, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.47, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 180.9, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.17, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 183.87, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.55, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 186.6, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.6, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 189.1, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.34, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 191.35, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.76, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 193.36, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 195.11, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.65, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 196.59, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 222.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 197.81, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 220.29, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 198.77, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 218.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 199.45, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 215.72, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 199.86, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 213.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 200.0, 30.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 210.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 203.0, 35.23, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 213.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 205.72, 40.45, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 215.72, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 208.15, 45.64, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 218.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 210.29, 50.79, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 220.29, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 212.12, 55.88, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 222.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 213.65, 60.9, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.65, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 214.86, 65.84, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 215.76, 70.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.76, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 216.34, 75.4, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.34, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 216.6, 80.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.6, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 216.55, 84.46, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.55, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 216.17, 88.78, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.17, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 215.47, 92.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.47, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 214.46, 96.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.46, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 213.14, 100.71, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.14, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 211.5, 104.31, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 221.5, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 209.56, 107.71, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 219.56, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 207.32, 110.9, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 217.32, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 204.78, 113.87, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 214.78, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 201.96, 116.6, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 211.96, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 198.86, 119.1, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 208.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 195.49, 121.35, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 205.49, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 191.85, 123.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 201.85, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 187.97, 125.11, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 197.97, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 183.84, 126.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 193.84, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 179.48, 127.81, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 189.48, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 174.9, 128.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 184.9, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 170.12, 129.45, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 180.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 165.15, 129.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 175.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 160.0, 130.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 30.0, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 104.49, 28.55, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 109.05, 27.33, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 113.67, 26.35, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 118.33, 25.62, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 123.02, 25.13, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 127.73, 24.89, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 132.45, 24.89, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 137.17, 25.15, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 141.86, 25.64, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 146.52, 26.39, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 151.13, 27.37, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 155.69, 28.6, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 160.18, 30.06, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 164.58, 31.76, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 168.89, 33.68, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 173.09, 35.83, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 177.18, 38.19, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 181.13, 40.77, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 184.95, 43.55, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 188.61, 46.52, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 192.12, 49.68, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 195.45, 53.02, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 198.6, 56.53, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 201.57, 60.2, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 204.34, 64.02, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 206.91, 67.98, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 209.26, 72.07, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 211.4, 76.28, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 213.31, 80.59, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 215.0, 85.0, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 216.45, 89.49, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 217.67, 94.05, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 218.65, 98.67, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 219.38, 103.33, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 219.87, 108.02, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 220.11, 112.73, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 220.11, 117.45, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 219.85, 122.17, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 219.36, 126.86, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 218.61, 131.52, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 217.63, 136.13, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 216.4, 140.69, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 214.94, 145.18, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 213.24, 149.58, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 211.32, 153.89, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 209.17, 158.09, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 206.81, 162.18, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 204.23, 166.13, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 201.45, 169.95, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 198.48, 173.61, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 195.32, 177.12, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 191.98, 180.45, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 188.47, 183.6, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 184.8, 186.57, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 180.98, 189.34, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 177.02, 191.91, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 172.93, 194.26, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 168.72, 196.4, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 164.41, 198.31, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 160.0, 200.0, 60.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [292.8, 460.0, 190.0, 0.0], + [701.7, 460.0, 190.0, 0.0], + [1097.7, 460.0, 190.0, 0.0], + [1479.8, 460.0, 190.0, 0.0], + [1846.9, 460.0, 190.0, 0.0], + [2197.9, 460.0, 190.0, 0.0], + [2531.9, 460.0, 190.0, 0.0], + [2848.1, 460.0, 190.0, 0.0], + [3145.4, 460.0, 190.0, 0.0], + [3423.2, 460.0, 190.0, 0.0], + [3680.7, 460.0, 190.0, 0.0], + [3917.0, 460.0, 190.0, 0.0], + [4131.7, 460.0, 190.0, 0.0], + [4324.1, 460.0, 190.0, 0.0], + [4493.7, 460.0, 190.0, 0.0], + [4640.0, 460.0, 190.0, 0.0], + [4762.6, 460.0, 190.0, 0.0], + [4861.2, 460.0, 190.0, 0.0], + [4935.5, 460.0, 190.0, 0.0], + [4985.3, 460.0, 190.0, 0.0], + [5010.5, 460.0, 190.0, 0.0], + [5011.0, 460.0, 190.0, 0.0], + [4986.8, 460.0, 190.0, 0.0], + [4643.4, 460.0, 190.0, 0.0], + [4176.6, 460.0, 190.0, 0.0], + [3691.6, 460.0, 190.0, 0.0], + [3196.7, 460.0, 190.0, 0.0], + [2700.0, 460.0, 190.0, 0.0], + [3196.7, 460.0, 190.0, 0.0], + [3691.6, 460.0, 190.0, 0.0], + [4176.6, 460.0, 190.0, 0.0], + [4643.4, 460.0, 190.0, 0.0], + [4986.8, 460.0, 190.0, 0.0], + [5011.0, 460.0, 190.0, 0.0], + [5010.5, 460.0, 190.0, 0.0], + [4985.3, 460.0, 190.0, 0.0], + [4935.5, 460.0, 190.0, 0.0], + [4861.2, 460.0, 190.0, 0.0], + [4762.6, 460.0, 190.0, 0.0], + [4640.0, 460.0, 190.0, 0.0], + [4493.7, 460.0, 190.0, 0.0], + [4324.1, 460.0, 190.0, 0.0], + [4131.7, 460.0, 190.0, 0.0], + [3917.0, 460.0, 190.0, 0.0], + [3680.7, 460.0, 190.0, 0.0], + [3423.2, 460.0, 190.0, 0.0], + [3145.4, 460.0, 190.0, 0.0], + [2848.1, 460.0, 190.0, 0.0], + [2531.9, 460.0, 190.0, 0.0], + [2197.9, 460.0, 190.0, 0.0], + [1846.9, 460.0, 190.0, 0.0], + [1479.8, 460.0, 190.0, 0.0], + [1097.7, 460.0, 190.0, 0.0], + [701.7, 460.0, 190.0, 0.0], + [292.8, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0], + [0.0, 460.0, 190.0, 0.0] + ] + } + }, + { + "id": "text", + "name": "rotated text in a row", + "labels": ["container", "a", "text+rot", "c"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 94.2, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 236.63, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 101.13, 92.24, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 186.63, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 237.44, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 102.26, 90.31, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 187.44, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.05, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 103.38, 88.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.05, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.44, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 104.49, 86.58, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.44, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.61, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 105.59, 84.78, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.61, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.57, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 106.67, 83.05, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.57, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.32, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 107.74, 81.37, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.32, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 237.85, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 108.79, 79.76, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 187.85, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 237.17, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 109.81, 78.22, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 187.17, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 236.27, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 110.8, 76.75, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 186.27, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.17, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 111.76, 75.36, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.17, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 233.86, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 112.7, 74.04, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 183.86, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 232.35, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 113.59, 72.82, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 182.35, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 230.63, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 114.45, 71.68, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 180.63, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 228.73, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 115.27, 70.63, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 178.73, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 226.64, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 116.05, 69.68, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 176.64, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 224.36, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 116.79, 68.83, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 174.36, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 221.91, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 117.47, 68.07, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 171.91, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 219.29, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 118.12, 67.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 169.29, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 216.51, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 118.71, 66.86, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 166.51, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 213.57, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 119.25, 66.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 163.57, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 210.48, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 119.73, 66.08, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 160.48, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 207.26, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 120.17, 65.84, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 157.26, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 203.9, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 120.54, 65.71, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 153.9, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 200.43, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 120.86, 65.69, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 150.43, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 196.85, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 121.13, 65.78, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 146.85, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 193.16, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 121.33, 65.98, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 143.16, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 189.38, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 121.48, 66.28, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 139.38, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 185.53, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 121.57, 66.69, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 135.53, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 181.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 121.6, 67.2, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 131.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 185.53, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 125.53, 67.82, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 135.53, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 189.38, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 129.38, 68.54, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 139.38, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 193.16, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 133.16, 69.35, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 143.16, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 196.85, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 136.85, 70.27, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 146.85, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 200.43, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 140.43, 71.28, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 150.43, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 203.9, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 143.9, 72.39, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 153.9, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 207.26, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 147.26, 73.58, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 157.26, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 210.48, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 150.48, 74.86, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 160.48, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 213.57, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 153.57, 76.22, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 163.57, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 216.51, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 156.51, 77.66, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 166.51, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 219.29, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 159.29, 79.18, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 169.29, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 221.91, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 161.91, 80.77, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 171.91, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 224.36, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 164.36, 82.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 174.36, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 226.64, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 166.64, 84.14, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 176.64, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 228.73, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 168.73, 85.91, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 178.73, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 230.63, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 170.63, 87.73, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 180.63, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 232.35, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 172.35, 89.6, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 182.35, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 233.86, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 173.86, 91.52, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 183.86, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.17, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 175.17, 93.47, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.17, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 236.27, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 176.27, 95.45, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 186.27, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 237.17, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 177.17, 97.46, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 187.17, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 237.85, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 177.85, 99.49, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 187.85, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.32, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 178.32, 101.54, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.32, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.57, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 178.57, 103.59, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.57, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.61, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 178.61, 105.65, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.61, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.44, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 178.44, 107.7, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.44, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 238.05, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 178.05, 109.75, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 188.05, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 237.44, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 177.44, 111.79, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 187.44, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 236.63, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 176.63, 113.81, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 186.63, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 175.6, 115.8, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 235.6, 170.0, 0.0], + [0.0, 236.6, 170.0, 0.0], + [0.0, 237.4, 170.0, 0.0], + [0.0, 238.0, 170.0, 0.0], + [0.0, 238.4, 170.0, 0.0], + [0.0, 238.6, 170.0, 0.0], + [0.0, 238.6, 170.0, 0.0], + [0.0, 238.3, 170.0, 0.0], + [0.0, 237.8, 170.0, 0.0], + [0.0, 237.2, 170.0, 0.0], + [0.0, 236.3, 170.0, 0.0], + [0.0, 235.2, 170.0, 0.0], + [0.0, 233.9, 170.0, 0.0], + [0.0, 232.3, 170.0, 0.0], + [0.0, 230.6, 170.0, 0.0], + [0.0, 228.7, 170.0, 0.0], + [0.0, 226.6, 170.0, 0.0], + [0.0, 224.4, 170.0, 0.0], + [0.0, 221.9, 170.0, 0.0], + [0.0, 219.3, 170.0, 0.0], + [0.0, 216.5, 170.0, 0.0], + [0.0, 213.6, 170.0, 0.0], + [0.0, 210.5, 170.0, 0.0], + [0.0, 207.3, 170.0, 0.0], + [0.0, 203.9, 170.0, 0.0], + [0.0, 200.4, 170.0, 0.0], + [0.0, 196.8, 170.0, 0.0], + [0.0, 193.2, 170.0, 0.0], + [0.0, 189.4, 170.0, 0.0], + [0.0, 185.5, 170.0, 0.0], + [0.0, 181.6, 170.0, 0.0], + [0.0, 185.5, 170.0, 0.0], + [0.0, 189.4, 170.0, 0.0], + [0.0, 193.2, 170.0, 0.0], + [0.0, 196.8, 170.0, 0.0], + [0.0, 200.4, 170.0, 0.0], + [0.0, 203.9, 170.0, 0.0], + [0.0, 207.3, 170.0, 0.0], + [0.0, 210.5, 170.0, 0.0], + [0.0, 213.6, 170.0, 0.0], + [0.0, 216.5, 170.0, 0.0], + [0.0, 219.3, 170.0, 0.0], + [0.0, 221.9, 170.0, 0.0], + [0.0, 224.4, 170.0, 0.0], + [0.0, 226.6, 170.0, 0.0], + [0.0, 228.7, 170.0, 0.0], + [0.0, 230.6, 170.0, 0.0], + [0.0, 232.3, 170.0, 0.0], + [0.0, 233.9, 170.0, 0.0], + [0.0, 235.2, 170.0, 0.0], + [0.0, 236.3, 170.0, 0.0], + [0.0, 237.2, 170.0, 0.0], + [0.0, 237.8, 170.0, 0.0], + [0.0, 238.3, 170.0, 0.0], + [0.0, 238.6, 170.0, 0.0], + [0.0, 238.6, 170.0, 0.0], + [0.0, 238.4, 170.0, 0.0], + [0.0, 238.0, 170.0, 0.0], + [0.0, 237.4, 170.0, 0.0], + [0.0, 236.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 94.2, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 100.62, 92.24, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 101.34, 90.31, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 102.15, 88.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 103.07, 86.58, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 104.08, 84.78, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 105.19, 83.05, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 106.38, 81.37, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 107.66, 79.76, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 109.02, 78.22, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 110.46, 76.75, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 111.98, 75.36, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 113.57, 74.04, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 115.22, 72.82, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 116.94, 71.68, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 118.71, 70.63, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 120.53, 69.68, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 122.4, 68.83, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 124.32, 68.07, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 126.27, 67.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 128.25, 66.86, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 130.26, 66.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 132.29, 66.08, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 134.34, 65.84, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 136.39, 65.71, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 138.45, 65.69, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 140.5, 65.78, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 142.55, 65.98, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 144.59, 66.28, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 146.61, 66.69, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 148.6, 67.2, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 150.56, 67.82, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 152.49, 68.54, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 154.38, 69.35, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 156.22, 70.27, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 158.02, 71.28, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 159.75, 72.39, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 161.43, 73.58, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 163.04, 74.86, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 164.58, 76.22, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 166.05, 77.66, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 167.45, 79.18, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 168.76, 80.77, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 169.98, 82.42, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 171.12, 84.14, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 172.17, 85.91, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 173.12, 87.73, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 173.97, 89.6, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 174.73, 91.52, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 175.38, 93.47, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 175.94, 95.45, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 176.38, 97.46, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 176.72, 99.49, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 176.96, 101.54, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 177.09, 103.59, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 177.11, 105.65, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 177.02, 107.7, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 176.82, 109.75, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 176.52, 111.79, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 176.11, 113.81, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 235.6, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 175.6, 115.8, 75.6, 21.6], + [1.0, 0.0, 0.0, 1.0, 185.6, 55.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0], + [0.0, 235.6, 170.0, 0.0] + ] + } + }, + { + "id": "hug", + "name": "hug containment", + "labels": ["container (hug)", "a", "b", "rot"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 225.15, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 175.23, 63.5, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 230.12, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 180.45, 62.14, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 234.9, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 185.64, 60.92, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 239.48, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 190.79, 59.86, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 243.84, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 195.88, 58.94, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 247.97, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 200.9, 58.18, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 251.85, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 205.84, 57.57, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 255.49, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 210.67, 57.12, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 258.86, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 215.4, 56.83, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 261.96, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 220.0, 56.7, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 264.78, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 224.46, 56.73, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 267.32, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 228.78, 56.92, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 269.56, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 232.93, 57.26, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 271.5, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 236.91, 57.77, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 273.14, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 240.71, 58.43, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.46, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 244.31, 59.25, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 275.47, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 247.71, 60.22, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.17, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 250.9, 61.34, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.55, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 253.87, 62.61, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.6, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 256.6, 64.02, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.34, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 259.1, 65.57, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 275.76, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 261.35, 67.26, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.86, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 263.36, 69.07, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 273.65, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 265.11, 71.02, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 272.12, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 266.59, 73.08, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 270.29, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 267.81, 75.26, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 268.15, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 268.77, 77.55, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 265.72, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 269.45, 79.94, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 263.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 269.86, 82.42, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 260.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 270.0, 85.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 263.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 273.0, 87.66, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 265.72, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 275.72, 90.39, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 268.15, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 278.15, 93.19, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 270.29, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 280.29, 96.05, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 272.12, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 282.12, 98.96, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 273.65, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 283.65, 101.92, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.86, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 284.86, 104.91, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 275.76, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 285.76, 107.93, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.34, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 286.34, 110.97, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.6, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 286.6, 114.02, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.55, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 286.55, 117.07, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.17, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 286.17, 120.12, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 275.47, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 285.47, 123.15, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.46, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 284.46, 126.16, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 273.14, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 283.14, 129.14, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 271.5, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 281.5, 132.08, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 269.56, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 279.56, 134.98, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 267.32, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 277.32, 137.82, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 264.78, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 274.78, 140.59, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 261.96, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 271.96, 143.3, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 258.86, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 268.86, 145.93, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 255.49, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 265.49, 148.48, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 251.85, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 261.85, 150.93, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 247.97, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 257.97, 153.28, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 243.84, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 253.84, 155.53, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 239.48, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 249.48, 157.67, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 234.9, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 244.9, 159.69, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 230.12, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 240.12, 161.59, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 225.15, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 235.15, 163.36, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 230.0, 165.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 220.0, 190.0, 0.0], + [0.0, 225.2, 190.0, 0.0], + [0.0, 230.1, 190.0, 0.0], + [0.0, 234.9, 190.0, 0.0], + [0.0, 239.5, 190.0, 0.0], + [0.0, 243.8, 190.0, 0.0], + [0.0, 248.0, 190.0, 0.0], + [0.0, 251.9, 190.0, 0.0], + [0.0, 255.5, 190.0, 0.0], + [0.0, 258.9, 190.0, 0.0], + [0.0, 262.0, 190.0, 0.0], + [0.0, 264.8, 190.0, 0.0], + [0.0, 267.3, 190.0, 0.0], + [0.0, 269.6, 190.0, 0.0], + [0.0, 271.5, 190.0, 0.0], + [0.0, 273.1, 190.0, 0.0], + [0.0, 274.5, 190.0, 0.0], + [0.0, 275.5, 190.0, 0.0], + [0.0, 276.2, 190.0, 0.0], + [0.0, 276.5, 190.0, 0.0], + [0.0, 276.6, 190.0, 0.0], + [0.0, 276.3, 190.0, 0.0], + [0.0, 275.8, 190.0, 0.0], + [0.0, 274.9, 190.0, 0.0], + [0.0, 273.6, 190.0, 0.0], + [0.0, 272.1, 190.0, 0.0], + [0.0, 270.3, 190.0, 0.0], + [0.0, 268.2, 190.0, 0.0], + [0.0, 265.7, 190.0, 0.0], + [0.0, 263.0, 190.0, 0.0], + [0.0, 260.0, 190.0, 0.0], + [0.0, 263.0, 190.0, 0.0], + [0.0, 265.7, 190.0, 0.0], + [0.0, 268.2, 190.0, 0.0], + [0.0, 270.3, 190.0, 0.0], + [0.0, 272.1, 190.0, 0.0], + [0.0, 273.6, 190.0, 0.0], + [0.0, 274.9, 190.0, 0.0], + [0.0, 275.8, 190.0, 0.0], + [0.0, 276.3, 190.0, 0.0], + [0.0, 276.6, 190.0, 0.0], + [0.0, 276.5, 190.0, 0.0], + [0.0, 276.2, 190.0, 0.0], + [0.0, 275.5, 190.0, 0.0], + [0.0, 274.5, 190.0, 0.0], + [0.0, 273.1, 190.0, 0.0], + [0.0, 271.5, 190.0, 0.0], + [0.0, 269.6, 190.0, 0.0], + [0.0, 267.3, 190.0, 0.0], + [0.0, 264.8, 190.0, 0.0], + [0.0, 262.0, 190.0, 0.0], + [0.0, 258.9, 190.0, 0.0], + [0.0, 255.5, 190.0, 0.0], + [0.0, 251.9, 190.0, 0.0], + [0.0, 248.0, 190.0, 0.0], + [0.0, 243.8, 190.0, 0.0], + [0.0, 239.5, 190.0, 0.0], + [0.0, 234.9, 190.0, 0.0], + [0.0, 230.1, 190.0, 0.0], + [0.0, 225.2, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 172.66, 63.5, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 175.39, 62.14, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 178.19, 60.92, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 181.05, 59.86, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 183.96, 58.94, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 186.92, 58.18, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 189.91, 57.57, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 192.93, 57.12, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 195.97, 56.83, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 199.02, 56.7, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 202.07, 56.73, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 205.12, 56.92, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 208.15, 57.26, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 211.16, 57.77, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 214.14, 58.43, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 217.08, 59.25, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 219.98, 60.22, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 222.82, 61.34, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 225.59, 62.61, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 228.3, 64.02, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 230.93, 65.57, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 233.48, 67.26, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 235.93, 69.07, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 238.28, 71.02, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 240.53, 73.08, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 242.67, 75.26, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 244.69, 77.55, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 246.59, 79.94, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 248.36, 82.42, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 250.0, 85.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 251.5, 87.66, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 252.86, 90.39, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 254.08, 93.19, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 255.14, 96.05, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 256.06, 98.96, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 256.82, 101.92, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 257.43, 104.91, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 257.88, 107.93, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 258.17, 110.97, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 258.3, 114.02, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 258.27, 117.07, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 258.08, 120.12, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 257.74, 123.15, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 257.23, 126.16, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 256.57, 129.14, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 255.75, 132.08, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 254.78, 134.98, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 253.66, 137.82, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 252.39, 140.59, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 250.98, 143.3, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 249.43, 145.93, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 247.74, 148.48, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 245.93, 150.93, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 243.98, 153.28, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 241.92, 155.53, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 239.74, 157.67, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 237.45, 159.69, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 235.06, 161.59, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 232.58, 163.36, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 230.0, 165.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [191.9, 220.0, 190.0, 1.9], + [398.3, 220.0, 190.0, 4.0], + [592.6, 220.0, 190.0, 5.9], + [774.3, 220.0, 190.0, 7.7], + [943.0, 220.0, 190.0, 9.4], + [1098.1, 220.0, 190.0, 11.0], + [1239.2, 220.0, 190.0, 12.4], + [1366.0, 220.0, 190.0, 13.7], + [1478.0, 220.0, 190.0, 14.8], + [1575.1, 220.0, 190.0, 15.8], + [1656.9, 220.0, 190.0, 16.6], + [1723.1, 220.0, 190.0, 17.2], + [1773.7, 220.0, 190.0, 17.7], + [1808.4, 220.0, 190.0, 18.1], + [1827.3, 220.0, 190.0, 18.3], + [1830.1, 220.0, 190.0, 18.3], + [1796.3, 220.0, 190.0, 18.2], + [1707.2, 220.0, 190.0, 17.9], + [1601.0, 220.0, 190.0, 17.4], + [1479.9, 220.0, 190.0, 16.8], + [1346.5, 220.0, 190.0, 16.1], + [1203.7, 220.0, 190.0, 15.1], + [1054.5, 220.0, 190.0, 14.1], + [901.9, 220.0, 190.0, 12.9], + [749.3, 220.0, 190.0, 11.5], + [600.0, 220.0, 190.0, 10.0], + [749.3, 220.0, 190.0, 11.5], + [901.9, 220.0, 190.0, 12.9], + [1054.5, 220.0, 190.0, 14.1], + [1203.7, 220.0, 190.0, 15.1], + [1346.5, 220.0, 190.0, 16.1], + [1479.9, 220.0, 190.0, 16.8], + [1601.0, 220.0, 190.0, 17.4], + [1707.2, 220.0, 190.0, 17.9], + [1796.3, 220.0, 190.0, 18.2], + [1830.1, 220.0, 190.0, 18.3], + [1827.3, 220.0, 190.0, 18.3], + [1808.4, 220.0, 190.0, 18.1], + [1773.7, 220.0, 190.0, 17.7], + [1723.1, 220.0, 190.0, 17.2], + [1656.9, 220.0, 190.0, 16.6], + [1575.1, 220.0, 190.0, 15.8], + [1478.0, 220.0, 190.0, 14.8], + [1366.0, 220.0, 190.0, 13.7], + [1239.2, 220.0, 190.0, 12.4], + [1098.1, 220.0, 190.0, 11.0], + [943.0, 220.0, 190.0, 9.4], + [774.3, 220.0, 190.0, 7.7], + [592.6, 220.0, 190.0, 5.9], + [398.3, 220.0, 190.0, 4.0], + [191.9, 220.0, 190.0, 1.9], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0], + [0.0, 220.0, 190.0, 0.0] + ] + } + }, + { + "id": "wrap", + "name": "wrap reflow", + "labels": ["container", "a", "rot", "c", "d"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 110.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 143.5], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [1.0, 0.052, -0.052, 0.999, 116.28, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 196.18, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 276.18, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 146.66], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.99, 0.105, -0.105, 0.995, 122.54, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 202.16, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 282.16, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 149.47], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.99, 0.156, -0.156, 0.988, 128.77, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 207.91, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 287.91, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 151.93], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.98, 0.208, -0.208, 0.978, 134.95, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 213.42, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 293.42, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 154.03], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.97, 0.259, -0.259, 0.966, 141.06, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 218.67, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 298.67, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 155.76], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.95, 0.309, -0.309, 0.951, 147.08, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 223.66, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 303.66, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 157.12], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.93, 0.358, -0.358, 0.934, 153.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 228.35, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 308.35, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.1], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.91, 0.407, -0.407, 0.914, 158.81, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 232.76, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 312.76, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.7], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.89, 0.454, -0.454, 0.891, 164.48, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 236.85, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 316.85, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.92], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.87, 0.5, -0.5, 0.866, 170.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 240.62, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.92, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.77], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.84, 0.545, -0.545, 0.839, 175.36, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 244.06, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.77, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.23], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.81, 0.588, -0.588, 0.809, 180.53, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 247.17, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.23, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 287.31], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.78, 0.629, -0.629, 0.777, 185.52, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 249.92, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 177.31, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 286.02], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.74, 0.669, -0.669, 0.743, 190.3, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 252.32, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 176.02, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 284.35], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.71, 0.707, -0.707, 0.707, 194.85, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 254.35, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 174.35, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 282.32], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.67, 0.743, -0.743, 0.669, 199.18, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 256.02, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 172.32, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 279.92], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.63, 0.777, -0.777, 0.629, 203.26, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 257.31, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 169.92, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 277.17], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.59, 0.809, -0.809, 0.588, 207.08, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.23, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 167.17, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 274.06], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.54, 0.839, -0.839, 0.545, 210.64, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.77, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 164.06, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.62], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.5, 0.866, -0.866, 0.5, 213.92, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.92, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.62, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.45, 0.891, -0.891, 0.454, 216.92, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.7, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.41, 0.914, -0.914, 0.407, 219.63, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.1, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.36, 0.934, -0.934, 0.358, 222.03, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 257.12, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.31, 0.951, -0.951, 0.309, 224.13, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 255.76, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.26, 0.966, -0.966, 0.259, 225.91, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 254.03, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.21, 0.978, -0.978, 0.208, 227.38, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 251.93, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.16, 0.988, -0.988, 0.156, 228.52, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 249.47, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.1, 0.995, -0.995, 0.105, 229.34, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 246.66, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.05, 0.999, -0.999, 0.052, 229.84, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 243.5, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.0, 1.0, -1.0, 0.0, 230.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 240.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 320.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.05, 0.999, -0.999, -0.052, 233.5, 36.28, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 243.5, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.1, 0.995, -0.995, -0.105, 236.66, 42.54, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 246.66, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.16, 0.988, -0.988, -0.156, 239.47, 48.77, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 249.47, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.21, 0.978, -0.978, -0.208, 241.93, 54.95, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 251.93, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.26, 0.966, -0.966, -0.259, 244.03, 61.06, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 254.03, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.31, 0.951, -0.951, -0.309, 245.76, 67.08, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 255.76, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.36, 0.934, -0.934, -0.358, 247.12, 73.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 257.12, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.41, 0.914, -0.914, -0.407, 248.1, 78.81, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.1, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.45, 0.891, -0.891, -0.454, 248.7, 84.48, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.7, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.62], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.5, 0.866, -0.866, -0.5, 248.92, 90.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.92, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.62, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 274.06], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.54, 0.839, -0.839, -0.545, 248.77, 95.36, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.77, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 164.06, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 277.17], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.59, 0.809, -0.809, -0.588, 248.23, 100.53, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 258.23, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 167.17, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 279.92], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.63, 0.777, -0.777, -0.629, 247.31, 105.52, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 257.31, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 169.92, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 282.32], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.67, 0.743, -0.743, -0.669, 246.02, 110.3, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 256.02, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 172.32, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 284.35], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.71, 0.707, -0.707, -0.707, 244.35, 114.85, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 254.35, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 174.35, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 286.02], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.74, 0.669, -0.669, -0.743, 242.32, 119.18, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 252.32, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 176.02, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 287.31], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.78, 0.629, -0.629, -0.777, 239.92, 123.26, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 249.92, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 177.31, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.23], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.81, 0.588, -0.588, -0.809, 237.17, 127.08, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 247.17, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.23, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.77], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.84, 0.545, -0.545, -0.839, 234.06, 130.64, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 244.06, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.77, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.92], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.87, 0.5, -0.5, -0.866, 230.62, 133.92, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 240.62, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.92, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.7], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.89, 0.454, -0.454, -0.891, 226.85, 136.92, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 236.85, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 316.85, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.1], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.91, 0.407, -0.407, -0.914, 222.76, 139.63, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 232.76, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 312.76, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 157.12], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.93, 0.358, -0.358, -0.934, 218.35, 142.03, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 228.35, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 308.35, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 155.76], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.95, 0.309, -0.309, -0.951, 213.66, 144.13, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 223.66, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 303.66, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 154.03], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.97, 0.259, -0.259, -0.966, 208.67, 145.91, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 218.67, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 298.67, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 151.93], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.98, 0.208, -0.208, -0.978, 203.42, 147.38, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 213.42, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 293.42, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 149.47], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.99, 0.156, -0.156, -0.988, 197.91, 148.52, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 207.91, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 287.91, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 146.66], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.99, 0.105, -0.105, -0.995, 192.16, 149.34, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 202.16, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 282.16, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 143.5], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-1.0, 0.052, -0.052, -0.999, 186.18, 149.84, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 196.18, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 276.18, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-1.0, 0.0, -0.0, -1.0, 180.0, 150.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ] + ], + "m": [ + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 143.5, 0.0], + [0.0, 380.0, 146.7, 0.0], + [0.0, 380.0, 149.5, 0.0], + [0.0, 380.0, 151.9, 0.0], + [0.0, 380.0, 154.0, 0.0], + [0.0, 380.0, 155.8, 0.0], + [0.0, 380.0, 157.1, 0.0], + [0.0, 380.0, 158.1, 0.0], + [0.0, 380.0, 158.7, 0.0], + [0.0, 380.0, 288.9, 0.0], + [0.0, 380.0, 288.8, 0.0], + [0.0, 380.0, 288.2, 0.0], + [0.0, 380.0, 287.3, 0.0], + [0.0, 380.0, 286.0, 0.0], + [0.0, 380.0, 284.4, 0.0], + [0.0, 380.0, 282.3, 0.0], + [0.0, 380.0, 279.9, 0.0], + [0.0, 380.0, 277.2, 0.0], + [0.0, 380.0, 274.1, 0.0], + [0.0, 380.0, 270.6, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.0, 0.0], + [0.0, 380.0, 270.6, 0.0], + [0.0, 380.0, 274.1, 0.0], + [0.0, 380.0, 277.2, 0.0], + [0.0, 380.0, 279.9, 0.0], + [0.0, 380.0, 282.3, 0.0], + [0.0, 380.0, 284.4, 0.0], + [0.0, 380.0, 286.0, 0.0], + [0.0, 380.0, 287.3, 0.0], + [0.0, 380.0, 288.2, 0.0], + [0.0, 380.0, 288.8, 0.0], + [0.0, 380.0, 288.9, 0.0], + [0.0, 380.0, 158.7, 0.0], + [0.0, 380.0, 158.1, 0.0], + [0.0, 380.0, 157.1, 0.0], + [0.0, 380.0, 155.8, 0.0], + [0.0, 380.0, 154.0, 0.0], + [0.0, 380.0, 151.9, 0.0], + [0.0, 380.0, 149.5, 0.0], + [0.0, 380.0, 146.7, 0.0], + [0.0, 380.0, 143.5, 0.0], + [0.0, 380.0, 140.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 110.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [1.0, 0.052, -0.052, 0.999, 113.19, 28.25, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.99, 0.105, -0.105, 0.995, 116.46, 26.67, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.99, 0.156, -0.156, 0.988, 119.82, 25.26, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.98, 0.208, -0.208, 0.978, 123.24, 24.03, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.97, 0.259, -0.259, 0.966, 126.72, 22.99, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.95, 0.309, -0.309, 0.951, 130.25, 22.12, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.93, 0.358, -0.358, 0.934, 133.83, 21.44, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.91, 0.407, -0.407, 0.914, 137.43, 20.95, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.89, 0.454, -0.454, 0.891, 141.05, 20.65, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.87, 0.5, -0.5, 0.866, 144.69, 20.54, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.84, 0.545, -0.545, 0.839, 148.32, 20.62, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.81, 0.588, -0.588, 0.809, 151.95, 20.89, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.78, 0.629, -0.629, 0.777, 155.56, 21.35, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.74, 0.669, -0.669, 0.743, 159.14, 21.99, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.71, 0.707, -0.707, 0.707, 162.68, 22.82, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.67, 0.743, -0.743, 0.669, 166.17, 23.84, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.63, 0.777, -0.777, 0.629, 169.6, 25.04, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.59, 0.809, -0.809, 0.588, 172.97, 26.42, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.54, 0.839, -0.839, 0.545, 176.26, 27.97, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.5, 0.866, -0.866, 0.5, 179.46, 29.69, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.45, 0.891, -0.891, 0.454, 182.57, 31.58, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.41, 0.914, -0.914, 0.407, 185.58, 33.62, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.36, 0.934, -0.934, 0.358, 188.47, 35.82, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.31, 0.951, -0.951, 0.309, 191.25, 38.17, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.26, 0.966, -0.966, 0.259, 193.9, 40.66, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.21, 0.978, -0.978, 0.208, 196.41, 43.29, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.16, 0.988, -0.988, 0.156, 198.79, 46.04, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.1, 0.995, -0.995, 0.105, 201.01, 48.92, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.05, 0.999, -0.999, 0.052, 203.09, 51.91, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [0.0, 1.0, -1.0, 0.0, 205.0, 55.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.05, 0.999, -0.999, -0.052, 206.75, 58.19, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.1, 0.995, -0.995, -0.105, 208.33, 61.46, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.16, 0.988, -0.988, -0.156, 209.74, 64.82, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.21, 0.978, -0.978, -0.208, 210.97, 68.24, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.26, 0.966, -0.966, -0.259, 212.01, 71.72, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.31, 0.951, -0.951, -0.309, 212.88, 75.25, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.36, 0.934, -0.934, -0.358, 213.56, 78.83, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.41, 0.914, -0.914, -0.407, 214.05, 82.43, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.45, 0.891, -0.891, -0.454, 214.35, 86.05, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.5, 0.866, -0.866, -0.5, 214.46, 89.69, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.54, 0.839, -0.839, -0.545, 214.38, 93.32, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.59, 0.809, -0.809, -0.588, 214.11, 96.95, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.63, 0.777, -0.777, -0.629, 213.65, 100.56, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.67, 0.743, -0.743, -0.669, 213.01, 104.14, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.71, 0.707, -0.707, -0.707, 212.18, 107.68, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.74, 0.669, -0.669, -0.743, 211.16, 111.17, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.78, 0.629, -0.629, -0.777, 209.96, 114.6, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.81, 0.588, -0.588, -0.809, 208.58, 117.97, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.84, 0.545, -0.545, -0.839, 207.03, 121.26, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.87, 0.5, -0.5, -0.866, 205.31, 124.46, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.89, 0.454, -0.454, -0.891, 203.42, 127.57, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.91, 0.407, -0.407, -0.914, 201.38, 130.58, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.93, 0.358, -0.358, -0.934, 199.18, 133.47, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.95, 0.309, -0.309, -0.951, 196.83, 136.25, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.97, 0.259, -0.259, -0.966, 194.34, 138.9, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.98, 0.208, -0.208, -0.978, 191.71, 141.41, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.99, 0.156, -0.156, -0.988, 188.96, 143.79, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-0.99, 0.105, -0.105, -0.995, 186.08, 146.01, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-1.0, 0.052, -0.052, -0.999, 183.09, 148.09, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0], + [-1.0, 0.0, -0.0, -1.0, 180.0, 150.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0] + ] + ], + "m": [ + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [205.2, 380.0, 140.0, 0.0], + [520.4, 380.0, 140.0, 0.0], + [819.4, 380.0, 140.0, 0.0], + [1101.3, 380.0, 140.0, 0.0], + [1365.4, 380.0, 140.0, 0.0], + [1611.0, 380.0, 140.0, 0.0], + [1837.3, 380.0, 140.0, 0.0], + [2043.8, 380.0, 140.0, 0.0], + [2229.9, 380.0, 140.0, 0.0], + [2395.1, 380.0, 140.0, 0.0], + [2539.0, 380.0, 140.0, 0.0], + [2661.0, 380.0, 140.0, 0.0], + [2761.0, 380.0, 140.0, 0.0], + [2838.6, 380.0, 140.0, 0.0], + [2893.6, 380.0, 140.0, 0.0], + [2925.9, 380.0, 140.0, 0.0], + [2935.4, 380.0, 140.0, 0.0], + [2845.3, 380.0, 140.0, 0.0], + [2711.6, 380.0, 140.0, 0.0], + [2552.6, 380.0, 140.0, 0.0], + [2371.5, 380.0, 140.0, 0.0], + [2172.2, 380.0, 140.0, 0.0], + [1958.6, 380.0, 140.0, 0.0], + [1735.0, 380.0, 140.0, 0.0], + [1506.0, 380.0, 140.0, 0.0], + [1276.1, 380.0, 140.0, 0.0], + [1050.0, 380.0, 140.0, 0.0], + [1276.1, 380.0, 140.0, 0.0], + [1506.0, 380.0, 140.0, 0.0], + [1735.0, 380.0, 140.0, 0.0], + [1958.6, 380.0, 140.0, 0.0], + [2172.2, 380.0, 140.0, 0.0], + [2371.5, 380.0, 140.0, 0.0], + [2552.6, 380.0, 140.0, 0.0], + [2711.6, 380.0, 140.0, 0.0], + [2845.3, 380.0, 140.0, 0.0], + [2935.4, 380.0, 140.0, 0.0], + [2925.9, 380.0, 140.0, 0.0], + [2893.6, 380.0, 140.0, 0.0], + [2838.6, 380.0, 140.0, 0.0], + [2761.0, 380.0, 140.0, 0.0], + [2661.0, 380.0, 140.0, 0.0], + [2538.9, 380.0, 140.0, 0.0], + [2395.1, 380.0, 140.0, 0.0], + [2229.9, 380.0, 140.0, 0.0], + [2043.8, 380.0, 140.0, 0.0], + [1837.3, 380.0, 140.0, 0.0], + [1611.0, 380.0, 140.0, 0.0], + [1365.4, 380.0, 140.0, 0.0], + [1101.3, 380.0, 140.0, 0.0], + [819.4, 380.0, 140.0, 0.0], + [520.4, 380.0, 140.0, 0.0], + [205.2, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0], + [0.0, 380.0, 140.0, 0.0] + ] + } + }, + { + "id": "between", + "name": "space-between", + "labels": ["container", "a", "rot", "c"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 220.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 222.66, 53.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 225.39, 52.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 228.19, 50.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 231.05, 49.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 233.96, 48.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 236.92, 48.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 239.91, 47.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 242.93, 47.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 245.97, 46.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 249.02, 46.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 252.07, 46.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 255.12, 46.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 258.15, 47.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 261.16, 47.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 264.14, 48.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 267.08, 49.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 269.98, 50.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 272.82, 51.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 275.59, 52.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 278.3, 54.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 280.93, 55.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 283.48, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 285.93, 59.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 288.28, 61.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 290.53, 63.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 292.67, 65.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 294.69, 67.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 296.59, 69.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 298.36, 72.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 300.0, 75.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 301.5, 77.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 302.86, 80.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 304.08, 83.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 305.14, 86.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 306.06, 88.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 306.82, 91.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 307.43, 94.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 307.88, 97.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 308.17, 100.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 308.3, 104.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 308.27, 107.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 308.08, 110.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 307.74, 113.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 307.23, 116.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 306.57, 119.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 305.75, 122.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 304.78, 124.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 303.66, 127.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 302.39, 130.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 300.98, 133.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 299.43, 135.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 297.74, 138.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 295.93, 140.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 293.98, 143.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 291.92, 145.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 289.74, 147.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 287.45, 149.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 285.06, 151.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 282.58, 153.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 280.0, 155.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 220.0, 55.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 222.66, 53.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 225.39, 52.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 228.19, 50.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 231.05, 49.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 233.96, 48.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 236.92, 48.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 239.91, 47.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 242.93, 47.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 245.97, 46.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 249.02, 46.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 252.07, 46.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 255.12, 46.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 258.15, 47.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 261.16, 47.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 264.14, 48.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 267.08, 49.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 269.98, 50.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 272.82, 51.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 275.59, 52.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 278.3, 54.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 280.93, 55.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 283.48, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 285.93, 59.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 288.28, 61.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 290.53, 63.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 292.67, 65.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 294.69, 67.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 296.59, 69.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 298.36, 72.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 300.0, 75.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 301.5, 77.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 302.86, 80.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 304.08, 83.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 305.14, 86.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 306.06, 88.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 306.82, 91.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 307.43, 94.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 307.88, 97.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 308.17, 100.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 308.3, 104.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 308.27, 107.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 308.08, 110.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 307.74, 113.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 307.23, 116.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 306.57, 119.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 305.75, 122.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 304.78, 124.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 303.66, 127.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 302.39, 130.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 300.98, 133.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 299.43, 135.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 297.74, 138.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 295.93, 140.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 293.98, 143.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 291.92, 145.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 289.74, 147.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 287.45, 149.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 285.06, 151.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 282.58, 153.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 280.0, 155.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0], + [0.0, 460.0, 170.0, 0.0] + ] + } + }, + { + "id": "lens", + "name": "two lanes in one row", + "labels": ["container", "a", "header-rot", "b", "lens-rot", "c"], + "anchor": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 240.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 105.23, 63.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 175.15, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 247.81, 63.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 315.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 110.45, 62.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 180.12, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 255.51, 62.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 320.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 115.64, 60.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 184.9, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 263.1, 60.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 324.9, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 120.79, 59.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 189.48, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 270.53, 59.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 329.48, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 125.88, 58.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 193.84, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 277.8, 58.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 333.84, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 130.9, 58.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 197.97, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 284.88, 58.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 337.97, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 135.84, 57.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 201.85, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 291.76, 57.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 341.85, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 140.67, 57.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 205.49, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 298.42, 57.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 345.49, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 145.4, 56.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 208.86, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 304.83, 56.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 348.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 150.0, 56.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 211.96, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 310.98, 56.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 351.96, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 154.46, 56.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 214.78, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 316.86, 56.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 354.78, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 158.78, 56.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 217.32, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 322.44, 56.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 357.32, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 162.93, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 219.56, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 327.71, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 359.56, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 166.91, 57.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 221.5, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 332.66, 57.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 361.5, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 170.71, 58.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.14, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 337.28, 58.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 363.14, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 174.31, 59.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.46, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 341.55, 59.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 364.46, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 177.71, 60.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.47, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 345.45, 60.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 365.47, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 180.9, 61.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.17, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 348.99, 61.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.17, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 183.87, 62.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.55, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 352.14, 62.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.55, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 186.6, 64.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.6, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 354.9, 64.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.6, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 189.1, 65.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.34, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 357.27, 65.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.34, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 191.35, 67.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.76, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 359.23, 67.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 365.76, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 193.36, 69.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.86, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 360.79, 69.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 364.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 195.11, 71.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.65, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 361.93, 71.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 363.65, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 196.59, 73.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 222.12, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 362.65, 73.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 362.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 197.81, 75.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 220.29, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 362.96, 75.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 360.29, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 198.77, 77.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 218.15, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 362.85, 77.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 358.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 199.45, 79.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 215.72, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 362.31, 79.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 355.72, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 199.86, 82.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 213.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 361.36, 82.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 353.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 200.0, 85.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 210.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 360.0, 85.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 350.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 203.0, 87.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 213.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 364.5, 87.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 353.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 205.72, 90.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 215.72, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 368.59, 90.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 355.72, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 208.15, 93.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 218.15, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 372.23, 93.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 358.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 210.29, 96.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 220.29, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 375.43, 96.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 360.29, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 212.12, 98.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 222.12, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 378.18, 98.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 362.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 213.65, 101.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.65, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 380.47, 101.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 363.65, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 214.86, 104.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.86, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 382.29, 104.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 364.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 215.76, 107.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.76, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 383.64, 107.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 365.76, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 216.34, 110.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.34, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 384.51, 110.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.34, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 216.6, 114.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.6, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 384.9, 114.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.6, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 216.55, 117.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.55, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 384.82, 117.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.55, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 216.17, 120.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 226.17, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 384.25, 120.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 366.17, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 215.47, 123.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 225.47, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 383.21, 123.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 365.47, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 214.46, 126.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 224.46, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 381.69, 126.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 364.46, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 213.14, 129.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 223.14, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 379.71, 129.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 363.14, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 211.5, 132.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 221.5, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 377.25, 132.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 361.5, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 209.56, 134.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 219.56, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 374.34, 134.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 359.56, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 207.32, 137.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 217.32, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 370.98, 137.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 357.32, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 204.78, 140.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 214.78, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 367.18, 140.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 354.78, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 201.96, 143.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 211.96, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 362.94, 143.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 351.96, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 198.86, 145.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 208.86, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 358.29, 145.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 348.86, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 195.49, 148.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 205.49, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 353.23, 148.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 345.49, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 191.85, 150.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 201.85, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 347.78, 150.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 341.85, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 187.97, 153.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 197.97, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 341.95, 153.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 337.97, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 183.84, 155.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 193.84, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 335.76, 155.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 333.84, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 179.48, 157.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 189.48, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 329.22, 157.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 329.48, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 174.9, 159.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 184.9, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 322.36, 159.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 324.9, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 170.12, 161.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 180.12, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 315.19, 161.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 320.12, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 165.15, 163.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 175.15, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 307.73, 163.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 315.15, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 160.0, 165.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 300.0, 165.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [191.9, 520.0, 190.0, 0.0], + [398.3, 520.0, 190.0, 0.0], + [592.6, 520.0, 190.0, 0.0], + [774.3, 520.0, 190.0, 0.0], + [943.0, 520.0, 190.0, 0.0], + [1098.1, 520.0, 190.0, 0.0], + [1239.2, 520.0, 190.0, 0.0], + [1366.0, 520.0, 190.0, 0.0], + [1478.0, 520.0, 190.0, 0.0], + [1575.1, 520.0, 190.0, 0.0], + [1656.9, 520.0, 190.0, 0.0], + [1723.1, 520.0, 190.0, 0.0], + [1773.7, 520.0, 190.0, 0.0], + [1808.4, 520.0, 190.0, 0.0], + [1827.3, 520.0, 190.0, 0.0], + [1830.1, 520.0, 190.0, 0.0], + [1796.3, 520.0, 190.0, 0.0], + [1707.2, 520.0, 190.0, 0.0], + [1601.0, 520.0, 190.0, 0.0], + [1479.9, 520.0, 190.0, 0.0], + [1346.5, 520.0, 190.0, 0.0], + [1203.7, 520.0, 190.0, 0.0], + [1054.5, 520.0, 190.0, 0.0], + [901.9, 520.0, 190.0, 0.0], + [749.3, 520.0, 190.0, 0.0], + [600.0, 520.0, 190.0, 0.0], + [749.3, 520.0, 190.0, 0.0], + [901.9, 520.0, 190.0, 0.0], + [1054.5, 520.0, 190.0, 0.0], + [1203.7, 520.0, 190.0, 0.0], + [1346.5, 520.0, 190.0, 0.0], + [1479.9, 520.0, 190.0, 0.0], + [1601.0, 520.0, 190.0, 0.0], + [1707.2, 520.0, 190.0, 0.0], + [1796.3, 520.0, 190.0, 0.0], + [1830.1, 520.0, 190.0, 0.0], + [1827.3, 520.0, 190.0, 0.0], + [1808.4, 520.0, 190.0, 0.0], + [1773.7, 520.0, 190.0, 0.0], + [1723.1, 520.0, 190.0, 0.0], + [1656.9, 520.0, 190.0, 0.0], + [1575.1, 520.0, 190.0, 0.0], + [1478.0, 520.0, 190.0, 0.0], + [1366.0, 520.0, 190.0, 0.0], + [1239.2, 520.0, 190.0, 0.0], + [1098.1, 520.0, 190.0, 0.0], + [943.0, 520.0, 190.0, 0.0], + [774.3, 520.0, 190.0, 0.0], + [592.6, 520.0, 190.0, 0.0], + [398.3, 520.0, 190.0, 0.0], + [191.9, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0] + ] + }, + "css": { + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 240.0, 65.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 102.66, 63.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [1.0, 0.052, -0.052, 0.999, 242.66, 63.5, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 105.39, 62.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.99, 0.105, -0.105, 0.995, 245.39, 62.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 108.19, 60.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.99, 0.156, -0.156, 0.988, 248.19, 60.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 111.05, 59.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.98, 0.208, -0.208, 0.978, 251.05, 59.86, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 113.96, 58.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.97, 0.259, -0.259, 0.966, 253.96, 58.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 116.92, 58.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.95, 0.309, -0.309, 0.951, 256.92, 58.18, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 119.91, 57.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.93, 0.358, -0.358, 0.934, 259.91, 57.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 122.93, 57.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.91, 0.407, -0.407, 0.914, 262.93, 57.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 125.97, 56.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.89, 0.454, -0.454, 0.891, 265.97, 56.83, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 129.02, 56.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.87, 0.5, -0.5, 0.866, 269.02, 56.7, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 132.07, 56.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.84, 0.545, -0.545, 0.839, 272.07, 56.73, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 135.12, 56.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.81, 0.588, -0.588, 0.809, 275.12, 56.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 138.15, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.78, 0.629, -0.629, 0.777, 278.15, 57.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 141.16, 57.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.74, 0.669, -0.669, 0.743, 281.16, 57.77, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 144.14, 58.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.71, 0.707, -0.707, 0.707, 284.14, 58.43, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 147.08, 59.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.67, 0.743, -0.743, 0.669, 287.08, 59.25, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 149.98, 60.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.63, 0.777, -0.777, 0.629, 289.98, 60.22, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 152.82, 61.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.59, 0.809, -0.809, 0.588, 292.82, 61.34, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 155.59, 62.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.54, 0.839, -0.839, 0.545, 295.59, 62.61, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 158.3, 64.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.5, 0.866, -0.866, 0.5, 298.3, 64.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 160.93, 65.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.45, 0.891, -0.891, 0.454, 300.93, 65.57, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 163.48, 67.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.41, 0.914, -0.914, 0.407, 303.48, 67.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 165.93, 69.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.36, 0.934, -0.934, 0.358, 305.93, 69.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 168.28, 71.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.31, 0.951, -0.951, 0.309, 308.28, 71.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 170.53, 73.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.26, 0.966, -0.966, 0.259, 310.53, 73.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 172.67, 75.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.21, 0.978, -0.978, 0.208, 312.67, 75.26, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 174.69, 77.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.16, 0.988, -0.988, 0.156, 314.69, 77.55, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 176.59, 79.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.1, 0.995, -0.995, 0.105, 316.59, 79.94, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 178.36, 82.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.05, 0.999, -0.999, 0.052, 318.36, 82.42, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 180.0, 85.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [0.0, 1.0, -1.0, 0.0, 320.0, 85.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 181.5, 87.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.05, 0.999, -0.999, -0.052, 321.5, 87.66, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 182.86, 90.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.1, 0.995, -0.995, -0.105, 322.86, 90.39, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 184.08, 93.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.16, 0.988, -0.988, -0.156, 324.08, 93.19, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 185.14, 96.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.21, 0.978, -0.978, -0.208, 325.14, 96.05, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 186.06, 98.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.26, 0.966, -0.966, -0.259, 326.06, 98.96, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 186.82, 101.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.31, 0.951, -0.951, -0.309, 326.82, 101.92, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 187.43, 104.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.36, 0.934, -0.934, -0.358, 327.43, 104.91, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 187.88, 107.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.41, 0.914, -0.914, -0.407, 327.88, 107.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 188.17, 110.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.45, 0.891, -0.891, -0.454, 328.17, 110.97, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 188.3, 114.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.5, 0.866, -0.866, -0.5, 328.3, 114.02, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 188.27, 117.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.54, 0.839, -0.839, -0.545, 328.27, 117.07, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 188.08, 120.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.59, 0.809, -0.809, -0.588, 328.08, 120.12, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 187.74, 123.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.63, 0.777, -0.777, -0.629, 327.74, 123.15, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 187.23, 126.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.67, 0.743, -0.743, -0.669, 327.23, 126.16, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 186.57, 129.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.71, 0.707, -0.707, -0.707, 326.57, 129.14, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 185.75, 132.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.74, 0.669, -0.669, -0.743, 325.75, 132.08, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 184.78, 134.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.78, 0.629, -0.629, -0.777, 324.78, 134.98, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 183.66, 137.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.81, 0.588, -0.588, -0.809, 323.66, 137.82, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 182.39, 140.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.84, 0.545, -0.545, -0.839, 322.39, 140.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 180.98, 143.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.87, 0.5, -0.5, -0.866, 320.98, 143.3, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 179.43, 145.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.89, 0.454, -0.454, -0.891, 319.43, 145.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 177.74, 148.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.91, 0.407, -0.407, -0.914, 317.74, 148.48, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 175.93, 150.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.93, 0.358, -0.358, -0.934, 315.93, 150.93, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 173.98, 153.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.95, 0.309, -0.309, -0.951, 313.98, 153.28, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 171.92, 155.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.97, 0.259, -0.259, -0.966, 311.92, 155.53, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 169.74, 157.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.98, 0.208, -0.208, -0.978, 309.74, 157.67, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 167.45, 159.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.99, 0.156, -0.156, -0.988, 307.45, 159.69, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 165.06, 161.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-0.99, 0.105, -0.105, -0.995, 305.06, 161.59, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 162.58, 163.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-1.0, 0.052, -0.052, -0.999, 302.58, 163.36, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 520.0, 190.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 160.0, 165.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0], + [-1.0, 0.0, -0.0, -1.0, 300.0, 165.0, 60.0, 100.0], + [1.0, 0.0, 0.0, 1.0, 310.0, 65.0, 60.0, 100.0] + ] + ], + "m": [ + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [191.9, 520.0, 190.0, 0.0], + [398.3, 520.0, 190.0, 0.0], + [592.6, 520.0, 190.0, 0.0], + [774.3, 520.0, 190.0, 0.0], + [943.0, 520.0, 190.0, 0.0], + [1098.1, 520.0, 190.0, 0.0], + [1239.2, 520.0, 190.0, 0.0], + [1366.0, 520.0, 190.0, 0.0], + [1478.0, 520.0, 190.0, 0.0], + [1575.1, 520.0, 190.0, 0.0], + [1656.9, 520.0, 190.0, 0.0], + [1723.1, 520.0, 190.0, 0.0], + [1773.7, 520.0, 190.0, 0.0], + [1808.4, 520.0, 190.0, 0.0], + [1827.3, 520.0, 190.0, 0.0], + [1830.1, 520.0, 190.0, 0.0], + [1796.3, 520.0, 190.0, 0.0], + [1707.2, 520.0, 190.0, 0.0], + [1601.0, 520.0, 190.0, 0.0], + [1479.9, 520.0, 190.0, 0.0], + [1346.5, 520.0, 190.0, 0.0], + [1203.7, 520.0, 190.0, 0.0], + [1054.5, 520.0, 190.0, 0.0], + [901.9, 520.0, 190.0, 0.0], + [749.3, 520.0, 190.0, 0.0], + [600.0, 520.0, 190.0, 0.0], + [749.3, 520.0, 190.0, 0.0], + [901.9, 520.0, 190.0, 0.0], + [1054.5, 520.0, 190.0, 0.0], + [1203.7, 520.0, 190.0, 0.0], + [1346.5, 520.0, 190.0, 0.0], + [1479.9, 520.0, 190.0, 0.0], + [1601.0, 520.0, 190.0, 0.0], + [1707.2, 520.0, 190.0, 0.0], + [1796.3, 520.0, 190.0, 0.0], + [1830.1, 520.0, 190.0, 0.0], + [1827.3, 520.0, 190.0, 0.0], + [1808.4, 520.0, 190.0, 0.0], + [1773.7, 520.0, 190.0, 0.0], + [1723.1, 520.0, 190.0, 0.0], + [1656.9, 520.0, 190.0, 0.0], + [1575.1, 520.0, 190.0, 0.0], + [1478.0, 520.0, 190.0, 0.0], + [1366.0, 520.0, 190.0, 0.0], + [1239.2, 520.0, 190.0, 0.0], + [1098.1, 520.0, 190.0, 0.0], + [943.0, 520.0, 190.0, 0.0], + [774.3, 520.0, 190.0, 0.0], + [592.6, 520.0, 190.0, 0.0], + [398.3, 520.0, 190.0, 0.0], + [191.9, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0], + [0.0, 520.0, 190.0, 0.0] + ] + } + } + ] +} diff --git a/archive/model-v2/anchor/dec0-fork/index.html b/archive/model-v2/anchor/dec0-fork/index.html new file mode 100644 index 00000000..2c2052e5 --- /dev/null +++ b/archive/model-v2/anchor/dec0-fork/index.html @@ -0,0 +1,6837 @@ +anchor — DEC-0: the framing fork, in action + + +
+
+
+ model-v2 · anchor · DEC-0 — 2026-07-07 +
+

The framing fork, in action.

+

+ The same seven scenes, the same slider, resolved twice by the same lab: + left = layout-visible rotation (anchor as locked by E1; + Figma’s framing) · right = visual-only rotation (the + CSS framing — rotation as post-layout paint transform). Every frame + is real resolver output; the meters are measured on resolved world bounds. + Left tax: the fill×rotation pops (DEC-1). Right tax: overlap, + frozen containment, and broken Figma parity. Watch the meters. +

+
+ +
+ +
+
+ θ = 0° + + +
+
+
+

+ layout-visible — anchor / Figma (E1) +

+ +
+ sibling overlap 0 px² + container 0 + ink escape 0 px +
+
+
+

visual-only — the CSS framing

+ +
+ sibling overlap 0 px² + container 0 + ink escape 0 px +
+
+
+
+
+
+ + diff --git a/archive/model-v2/anchor/dec0-visual-only.md b/archive/model-v2/anchor/dec0-visual-only.md new file mode 100644 index 00000000..2e276566 --- /dev/null +++ b/archive/model-v2/anchor/dec0-visual-only.md @@ -0,0 +1,109 @@ +# DEC-0 · visual-only, normative + +2026-07-07. The rule set for the flipped default — **rotation (and every +header transform) is a post-layout paint transform; sizing never reads +it; reads stay oriented** — written as the spec review DEC-0's flip was +gated on. Each rule names its consequence and its guarding test +(`lab/tests/visual_only.rs` unless noted). + +## The two tiers (the one sentence) + +- **Sizing tier** (phase M measure, phase L layout: flex contributions, + hug, spans, derived unions): reads the **untransformed box** — + rotation and flips do not exist here. CSS-pure: no exceptions, no + hybrid hug. +- **Read tier** (phase B and everything downstream: `world_aabb`, + selection chrome, hit-testing, culling): **oriented, always** — even + CSS reports post-transform bounds for reads. + +`xywh` (the box read) belongs to the sizing tier: it reports the stable +basis, not the ink. Ink bounds are `world_aabb`. Two named reads, two +meanings, both spec'd — never one value doing both jobs. + +## Rules + +- **V-1 · flex contribution** — a rotated in-flow child contributes its + unrotated box; slot = box; rotation paints about the box center. + Siblings do not make room; overlap is correct behavior. + (`r3` sheet column, `v1_*`) +- **V-2 · fill never fights rotation** — `grow` and `self_align:Stretch` + apply to the unrotated box regardless of rotation; continuous at + every angle by construction. E-A4/E-A11/E-A12 retire; DEC-1/DEC-2/ + DEC-3 close n/a. (`v2_*`) +- **V-3 · hug ignores transforms** — auto-sized containers (flex hug + AND free-context hug) measure children's unrotated boxes at their + pins. A rotated child's ink may escape its hug parent; that is the + accepted CSS wart, and the escape is visible in the read tier + (`world_aabb` ⊃ box). (`v3_*`) Measured by the fork battery + ([`dec0-fork/`](./dec0-fork)) as `escape` = the greatest distance any + sibling's world-AABB runs past the container's **oriented** painted + rect (0 = fully contained); `overlap` = the greatest pairwise + world-AABB intersection area among siblings, in px². +- **V-4 · THE GROUP-BOX FORK, decided: derived boxes are sizing-tier.** + A group/lens box = union of members' **unrotated** boxes at their + pins (translation only — member rotation AND flips ignored). Chosen + over the oriented union because the derived box feeds flex/hug: an + oriented union would smuggle the envelope back into layout one + nesting level deep — the exact behavior DEC-0 removes. Consequences: + `xywh` of a group under-reports rotated ink (by design; ink = + `world_aabb`); selection chrome for derived kinds SHOULD use ink + bounds (spike HUD does). E-A1 origin placement and D-2 sibling + stability are untouched (the union is still union-shaped, just + unrotated). (`v4_*`) +- **V-5 · the group's own transform** — a group's OWN rotation/flip + still paints (origin pivot, E-A1/B1 unchanged) and still does not + feed its parent's sizing (the group contributes its unrotated union + box). (`v4_group_own_rotation_paints_only`) +- **V-6 · rotated parent rigidity (unchanged)** — layout runs in the + parent's local space; the assembly transforms as one body. Identical + in both arms and in CSS. (existing `rotation.rs` arm-pinned) +- **V-7 · the lens (unchanged mechanics, demoted distinction)** — + lens ops stay post-resolution paint; with header rotation now also + paint-only, `lens-rotate ≡ header-rotate` behaviorally. E-A8's lint + inverts: a lens containing ONLY `Rotate` is always redundant — use + header rotation. The lens remains the quarantine for shear/matrix/ + retained-scale. (existing `derived.rs` lens tests, arm-pinned where + needed) +- **V-8 · reads stay oriented** — `world_aabb`, pick, HUD chrome + geometry: unchanged from the anchor arm; flip/rotation fully visible + here. (existing `arena_pick.rs`, `geometry.rs`) +- **V-9 · reports** — the rotated-inert report class + (grow/stretch-ignored-when-rotated) must NOT fire: nothing is ignored + anymore. §8's matrix loses those rows. (`v2_no_inert_reports`) +- **V-10 · importer posture (COMPAT)** — Figma rotated-in-auto-layout + children import as `flow:Absolute` + pins at their resolved position + (geometry-exact, flow participation dropped); frequency to be + measured by E9. CSS `transform:rotate` imports 1:1 (this framing IS + CSS's). + +## What stays true from the anchor run (unchanged laws) + +E-A1 origins · E-A2/E-A14 flip semantics (flip was ALREADY +layout-invisible for boxed kinds; V-4 extends "sizing ignores flips" to +derived members) · E-A3 two stretches · E-A5 underdetermined bindings · +E-A6 format RMW (DEC-7 open) · E-A9/E-A10 points & scales · E-A13 group +constraint pass-through (orthogonal to rotation) · the ops layer, +write counts, typed walls · the IR · free context (no sizing consumer +of rotation existed there anyway — hug aside, which V-3 now covers). + +## Retirements + +- **E-A4, E-A11, E-A12** (grow/stretch × rotation policy) — the + configurations are legal and continuous now; nothing to declare. +- **E-A7** (envelope readout as layout explainer) — demoted to + selection info: basis vs ink is still worth showing, but mid-turn + slot width no longer changes, so nothing "reads as a bug". +- **E-A8** (context-scoped lens-rotate lint) — inverted, see V-7. +- **DEC-1, DEC-2, DEC-3** — closed n/a in the register. +- The dec0-fork and edge-cases artifacts remain as the DECISION RECORD + (they show both arms; the left panel is now the road not taken). + +## Review verdict + +The flip is semantics-narrow (one resolver read policy + the V-4 +definition) but the V-4 fork was REAL undefined behavior until decided +— a group with rotated members had four consumers reading one value +with two possible meanings. Decided sizing-tier; the suite now pins +both arms explicitly (the anchor arm's laws remain tested as the +documented alternative), and `visual_only.rs` is the new default's +conformance floor. diff --git a/archive/model-v2/anchor/edge-cases/frames.json b/archive/model-v2/anchor/edge-cases/frames.json new file mode 100644 index 00000000..f39fbe7c --- /dev/null +++ b/archive/model-v2/anchor/edge-cases/frames.json @@ -0,0 +1,3609 @@ +{ + "step": 3, + "scenes": [ + { + "id": "grow", + "name": "grow × rotation (E-A4)", + "labels": ["container", "a", "grow+rot", "c"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 55.0, 300.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 222.658, 53.5, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 225.391, 52.14, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 228.191, 50.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 231.051, 49.86, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 233.963, 48.94, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 236.919, 48.18, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 239.911, 47.57, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 242.93, 47.12, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 245.969, 46.83, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 249.019, 46.7, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 252.072, 46.73, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 255.119, 46.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 258.152, 47.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 261.162, 47.77, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 264.142, 48.43, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 267.083, 49.25, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 269.978, 50.22, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 272.817, 51.34, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 275.594, 52.61, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 278.301, 54.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 280.931, 55.57, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 283.475, 57.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 285.928, 59.07, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 288.282, 61.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 290.532, 63.08, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 292.67, 65.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 294.691, 67.55, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 296.59, 69.94, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 298.361, 72.42, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 300.0, 75.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 301.502, 77.66, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 302.862, 80.39, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 304.077, 83.19, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 305.145, 86.05, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 306.061, 88.96, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 306.823, 91.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 307.43, 94.91, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 307.879, 97.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 308.17, 100.97, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 308.301, 104.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 308.273, 107.07, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 308.084, 110.12, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 307.737, 113.15, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 307.231, 116.16, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 306.569, 119.14, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 305.751, 122.08, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 304.78, 124.98, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 303.66, 127.82, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 302.392, 130.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 300.981, 133.3, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 299.43, 135.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 297.743, 138.48, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 295.926, 140.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 293.983, 143.28, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 291.919, 145.53, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 289.74, 147.67, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 287.452, 149.69, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 285.062, 151.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 282.576, 153.36, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 280.0, 155.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ] + ] + }, + { + "id": "stretch", + "name": "stretch × rotation", + "labels": ["container", "a", "stretch+rot", "c"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 30.0, 60.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 105.234, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.151, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 110.453, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 180.124, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 115.643, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 184.905, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 120.791, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 189.48, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 125.882, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 193.837, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 130.902, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 197.965, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 135.837, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 201.852, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 140.674, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 205.486, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 145.399, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 208.859, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 150.0, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 211.962, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 154.464, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 214.784, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 158.779, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 217.32, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 162.932, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 219.561, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 166.913, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 221.502, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 170.711, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 223.137, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 174.314, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.462, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 177.715, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 225.474, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 180.902, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.169, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 183.867, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.545, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 186.603, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.603, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 189.101, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.34, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 191.355, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 225.759, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 193.358, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.86, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 195.106, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 223.647, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 196.593, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.122, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 197.815, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 220.289, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 198.769, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 218.155, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 199.452, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 215.724, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 199.863, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 213.003, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 200.0, 30.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 210.0, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 203.003, 35.23, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 213.003, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 205.724, 40.45, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 215.724, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 208.155, 45.64, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 218.155, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 210.289, 50.79, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 220.289, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 212.122, 55.88, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.122, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 213.647, 60.9, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 223.647, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 214.86, 65.84, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.86, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 215.759, 70.67, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 225.759, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 216.34, 75.4, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.34, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 216.603, 80.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.603, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 216.545, 84.46, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.545, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 216.169, 88.78, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.169, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 215.474, 92.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 225.474, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 214.462, 96.91, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.462, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 213.137, 100.71, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 223.137, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 211.502, 104.31, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 221.502, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 209.561, 107.71, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 219.561, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 207.32, 110.9, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 217.32, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 204.784, 113.87, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 214.784, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 201.962, 116.6, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 211.962, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 198.859, 119.1, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 208.859, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 195.486, 121.35, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 205.486, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 191.852, 123.36, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 201.852, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 187.965, 125.11, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 197.965, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 183.837, 126.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 193.837, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 179.48, 127.81, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 189.48, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 174.905, 128.77, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 184.905, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 170.124, 129.45, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 180.124, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 165.151, 129.86, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.151, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 160.0, 130.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0, 0.0] + ] + ] + }, + { + "id": "text", + "name": "rotated text in flow", + "labels": ["container", "a", "text+rot", "c"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 214.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 94.2, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 164.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 215.06, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 101.13, 92.8, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 165.056, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 215.96, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 102.258, 91.44, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 165.962, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 216.71, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 103.379, 90.11, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 166.714, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.31, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 104.491, 88.82, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.311, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.75, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 105.59, 87.58, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.75, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 218.03, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 106.675, 86.39, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 168.032, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 218.15, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 107.741, 85.24, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 168.154, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 218.12, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 108.785, 84.15, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 168.117, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.92, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 109.806, 83.12, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.921, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.57, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 110.8, 82.15, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.565, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.05, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 111.764, 81.24, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.052, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 216.38, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 112.696, 80.39, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 166.383, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 215.56, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 113.593, 79.62, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 165.559, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 214.58, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 114.453, 78.91, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 164.583, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 213.46, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 115.274, 78.27, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 163.457, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 212.18, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 116.052, 77.71, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 162.185, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 210.77, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 116.786, 77.22, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 160.77, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 209.22, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 117.475, 76.81, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 159.215, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 207.53, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 118.115, 76.47, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 157.526, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 205.71, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 118.706, 76.22, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 155.706, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 203.76, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 119.246, 76.04, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 153.761, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 201.7, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 119.733, 75.94, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 151.696, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 199.52, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 120.165, 75.92, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 149.517, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 197.23, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 120.543, 75.98, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 147.23, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 194.84, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 120.864, 76.12, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 144.84, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 192.36, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 121.128, 76.34, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 142.355, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 189.78, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 121.334, 76.64, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 139.782, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 187.13, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 121.482, 77.02, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 137.126, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 184.4, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 121.57, 77.47, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 134.397, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 181.6, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 121.6, 78.0, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 131.6, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 184.4, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 124.397, 78.6, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 134.397, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 187.13, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 127.126, 79.28, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 137.126, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 189.78, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 129.782, 80.02, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 139.782, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 192.36, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 132.355, 80.84, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 142.355, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 194.84, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 134.84, 81.72, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 144.84, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 197.23, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 137.23, 82.66, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 147.23, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 199.52, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 139.517, 83.66, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 149.517, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 201.7, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 141.696, 84.73, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 151.696, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 203.76, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 143.761, 85.85, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 153.761, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 205.71, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 145.706, 87.02, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 155.706, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 207.53, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 147.526, 88.24, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 157.526, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 209.22, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 149.215, 89.5, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 159.215, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 210.77, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 150.77, 90.81, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 160.77, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 212.18, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 152.185, 92.16, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 162.185, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 213.46, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 153.457, 93.54, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 163.457, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 214.58, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 154.583, 94.96, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 164.583, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 215.56, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 155.559, 96.4, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 165.559, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 216.38, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 156.383, 97.87, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 166.383, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.05, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 157.052, 99.35, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.052, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.57, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 157.565, 100.85, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.565, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.92, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 157.921, 102.37, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.921, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 218.12, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 158.117, 103.88, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 168.117, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 218.15, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 158.154, 105.41, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 168.154, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 218.03, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 158.032, 106.93, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 168.032, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.75, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 157.75, 108.44, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.75, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 217.31, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 157.311, 109.95, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 167.311, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 216.71, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 156.714, 111.44, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 166.714, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 215.96, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 155.962, 112.92, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 165.962, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 215.06, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 155.056, 114.37, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 165.056, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 214.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 154.0, 115.8, 54.0, 21.6, 0.0], + [1.0, 0.0, 0.0, 1.0, 164.0, 55.0, 60.0, 100.0, 0.0] + ] + ] + }, + { + "id": "rotframe", + "name": "rotated flex frame (rigid)", + "labels": ["flex frame (rotated)", "a", "grow", "c"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 90.0, 60.0, 320.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 75.0, 80.0, 90.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 75.0, 120.0, 90.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 320.0, 75.0, 80.0, 90.0, 0.0] + ], + [ + [1.0, 0.052, -0.052, 0.999, 93.359, 51.71, 320.0, 120.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 102.561, 67.21, 80.0, 90.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 192.437, 71.92, 120.0, 90.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 322.259, 78.73, 80.0, 90.0, 0.0] + ], + [ + [0.99, 0.105, -0.105, 0.995, 97.148, 43.6, 320.0, 120.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 105.525, 59.57, 80.0, 90.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 195.032, 68.97, 120.0, 90.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 324.32, 82.56, 80.0, 90.0, 0.0] + ], + [ + [0.99, 0.156, -0.156, 0.988, 101.356, 35.71, 320.0, 120.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 108.886, 52.09, 80.0, 90.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 197.778, 66.17, 120.0, 90.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 326.178, 86.5, 80.0, 90.0, 0.0] + ], + [ + [0.98, 0.208, -0.208, 0.978, 105.971, 28.05, 320.0, 120.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 112.634, 44.8, 80.0, 90.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 200.667, 63.51, 120.0, 90.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 327.826, 90.54, 80.0, 90.0, 0.0] + ], + [ + [0.97, 0.259, -0.259, 0.966, 110.981, 20.63, 320.0, 120.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 116.758, 37.71, 80.0, 90.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 203.691, 61.0, 120.0, 90.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 329.262, 94.65, 80.0, 90.0, 0.0] + ], + [ + [0.95, 0.309, -0.309, 0.951, 116.372, 13.49, 320.0, 120.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 121.247, 30.85, 80.0, 90.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 206.842, 58.66, 120.0, 90.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 330.48, 98.83, 80.0, 90.0, 0.0] + ], + [ + [0.93, 0.358, -0.358, 0.934, 122.129, 6.65, 320.0, 120.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 126.089, 24.23, 80.0, 90.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 210.112, 56.49, 120.0, 90.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 331.477, 103.07, 80.0, 90.0, 0.0] + ], + [ + [0.91, 0.407, -0.407, 0.914, 128.237, 0.11, 320.0, 120.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 131.271, 17.88, 80.0, 90.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 213.49, 54.49, 120.0, 90.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 332.251, 107.36, 80.0, 90.0, 0.0] + ], + [ + [0.89, 0.454, -0.454, 0.891, 134.678, -6.1, 320.0, 120.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 136.779, 11.81, 80.0, 90.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 216.969, 52.67, 120.0, 90.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 332.8, 111.68, 80.0, 90.0, 0.0] + ], + [ + [0.87, 0.5, -0.5, 0.866, 141.436, -11.96, 320.0, 120.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 142.596, 6.03, 80.0, 90.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 220.538, 51.03, 120.0, 90.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 333.122, 116.03, 80.0, 90.0, 0.0] + ], + [ + [0.84, 0.545, -0.545, 0.839, 148.491, -17.46, 320.0, 120.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 148.708, 0.56, 80.0, 90.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 224.189, 49.58, 120.0, 90.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 333.216, 120.38, 80.0, 90.0, 0.0] + ], + [ + [0.81, 0.588, -0.588, 0.809, 155.824, -22.59, 320.0, 120.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 155.098, -4.57, 80.0, 90.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 227.909, 48.33, 120.0, 90.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 333.082, 124.74, 80.0, 90.0, 0.0] + ], + [ + [0.78, 0.629, -0.629, 0.777, 163.416, -27.32, 320.0, 120.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 161.748, -9.37, 80.0, 90.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 231.691, 47.27, 120.0, 90.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 332.72, 129.08, 80.0, 90.0, 0.0] + ], + [ + [0.74, 0.669, -0.669, 0.743, 171.245, -31.65, 320.0, 120.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 168.639, -13.81, 80.0, 90.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 235.522, 46.41, 120.0, 90.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 332.131, 133.4, 80.0, 90.0, 0.0] + ], + [ + [0.71, 0.707, -0.707, 0.707, 179.289, -35.56, 320.0, 120.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 175.754, -17.89, 80.0, 90.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 239.393, 45.75, 120.0, 90.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 331.317, 137.68, 80.0, 90.0, 0.0] + ], + [ + [0.67, 0.743, -0.743, 0.669, 187.528, -39.05, 320.0, 120.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 183.072, -21.58, 80.0, 90.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 243.294, 45.3, 120.0, 90.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 330.281, 141.91, 80.0, 90.0, 0.0] + ], + [ + [0.63, 0.777, -0.777, 0.629, 195.937, -42.1, 320.0, 120.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 190.574, -24.89, 80.0, 90.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 247.212, 45.05, 120.0, 90.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 329.024, 146.08, 80.0, 90.0, 0.0] + ], + [ + [0.59, 0.809, -0.809, 0.588, 204.495, -44.71, 320.0, 120.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 198.238, -27.8, 80.0, 90.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 251.139, 45.01, 120.0, 90.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 327.551, 150.18, 80.0, 90.0, 0.0] + ], + [ + [0.54, 0.839, -0.839, 0.545, 213.178, -46.87, 320.0, 120.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 206.044, -30.31, 80.0, 90.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 255.062, 45.17, 120.0, 90.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 325.865, 154.2, 80.0, 90.0, 0.0] + ], + [ + [0.5, 0.866, -0.866, 0.5, 221.962, -48.56, 320.0, 120.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 213.971, -32.4, 80.0, 90.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 258.971, 45.54, 120.0, 90.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 323.971, 158.12, 80.0, 90.0, 0.0] + ], + [ + [0.45, 0.891, -0.891, 0.454, 230.822, -49.8, 320.0, 120.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 221.997, -34.08, 80.0, 90.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 262.856, 46.11, 120.0, 90.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 321.875, 161.94, 80.0, 90.0, 0.0] + ], + [ + [0.41, 0.914, -0.914, 0.407, 239.735, -50.57, 320.0, 120.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 230.099, -35.33, 80.0, 90.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 266.705, 46.88, 120.0, 90.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 319.581, 165.65, 80.0, 90.0, 0.0] + ], + [ + [0.36, 0.934, -0.934, 0.358, 248.676, -50.87, 320.0, 120.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 238.256, -36.16, 80.0, 90.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 270.509, 47.86, 120.0, 90.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 317.097, 169.22, 80.0, 90.0, 0.0] + ], + [ + [0.31, 0.951, -0.951, 0.309, 257.621, -50.71, 320.0, 120.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 246.445, -36.56, 80.0, 90.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 274.257, 49.03, 120.0, 90.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 314.429, 172.67, 80.0, 90.0, 0.0] + ], + [ + [0.26, 0.966, -0.966, 0.259, 266.544, -50.08, 320.0, 120.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 254.644, -36.54, 80.0, 90.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 277.938, 50.4, 120.0, 90.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 311.584, 175.97, 80.0, 90.0, 0.0] + ], + [ + [0.21, 0.978, -0.978, 0.208, 275.423, -48.98, 320.0, 120.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 262.83, -36.08, 80.0, 90.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 281.542, 51.96, 120.0, 90.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 308.57, 179.11, 80.0, 90.0, 0.0] + ], + [ + [0.16, 0.988, -0.988, 0.156, 284.232, -47.42, 320.0, 120.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 270.981, -35.19, 80.0, 90.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 285.06, 53.7, 120.0, 90.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 305.396, 182.1, 80.0, 90.0, 0.0] + ], + [ + [0.1, 0.995, -0.995, 0.105, 292.947, -45.4, 320.0, 120.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 279.074, -33.88, 80.0, 90.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 288.482, 55.62, 120.0, 90.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 302.07, 184.91, 80.0, 90.0, 0.0] + ], + [ + [0.05, 0.999, -0.999, 0.052, 301.544, -42.92, 320.0, 120.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 287.088, -32.15, 80.0, 90.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 291.798, 57.73, 120.0, 90.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 298.602, 187.55, 80.0, 90.0, 0.0] + ], + [ + [0.0, 1.0, -1.0, 0.0, 310.0, -40.0, 320.0, 120.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 295.0, -30.0, 80.0, 90.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 295.0, 60.0, 120.0, 90.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 295.0, 190.0, 80.0, 90.0, 0.0] + ], + [ + [-0.05, 0.999, -0.999, -0.052, 318.292, -36.64, 320.0, 120.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 302.789, -27.44, 80.0, 90.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 298.078, 62.44, 120.0, 90.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 291.275, 192.26, 80.0, 90.0, 0.0] + ], + [ + [-0.1, 0.995, -0.995, -0.105, 326.396, -32.85, 320.0, 120.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 310.433, -24.47, 80.0, 90.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 301.025, 65.03, 120.0, 90.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 287.436, 194.32, 80.0, 90.0, 0.0] + ], + [ + [-0.16, 0.988, -0.988, -0.156, 334.291, -28.64, 320.0, 120.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 317.911, -21.11, 80.0, 90.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 303.832, 67.78, 120.0, 90.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 283.496, 196.18, 80.0, 90.0, 0.0] + ], + [ + [-0.21, 0.978, -0.978, -0.208, 341.955, -24.03, 320.0, 120.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 325.203, -17.37, 80.0, 90.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 306.491, 70.67, 120.0, 90.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 279.463, 197.83, 80.0, 90.0, 0.0] + ], + [ + [-0.26, 0.966, -0.966, -0.259, 349.367, -19.02, 320.0, 120.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 332.289, -13.24, 80.0, 90.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 308.996, 73.69, 120.0, 90.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 275.349, 199.26, 80.0, 90.0, 0.0] + ], + [ + [-0.31, 0.951, -0.951, -0.309, 356.506, -13.63, 320.0, 120.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 339.15, -8.75, 80.0, 90.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 311.339, 76.84, 120.0, 90.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 271.166, 200.48, 80.0, 90.0, 0.0] + ], + [ + [-0.36, 0.934, -0.934, -0.358, 363.354, -7.87, 320.0, 120.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 345.766, -3.91, 80.0, 90.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 313.513, 80.11, 120.0, 90.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 266.925, 201.48, 80.0, 90.0, 0.0] + ], + [ + [-0.41, 0.914, -0.914, -0.407, 369.891, -1.76, 320.0, 120.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 352.12, 1.27, 80.0, 90.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 315.514, 83.49, 120.0, 90.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 262.638, 202.25, 80.0, 90.0, 0.0] + ], + [ + [-0.45, 0.891, -0.891, -0.454, 376.099, 4.68, 320.0, 120.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 358.194, 6.78, 80.0, 90.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 317.335, 86.97, 120.0, 90.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 258.316, 202.8, 80.0, 90.0, 0.0] + ], + [ + [-0.5, 0.866, -0.866, -0.5, 381.962, 11.44, 320.0, 120.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 363.971, 12.6, 80.0, 90.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 318.971, 90.54, 120.0, 90.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 253.971, 203.12, 80.0, 90.0, 0.0] + ], + [ + [-0.54, 0.839, -0.839, -0.545, 387.462, 18.49, 320.0, 120.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 369.436, 18.71, 80.0, 90.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 320.419, 94.19, 120.0, 90.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 249.615, 203.22, 80.0, 90.0, 0.0] + ], + [ + [-0.59, 0.809, -0.809, -0.588, 392.587, 25.82, 320.0, 120.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 374.574, 25.1, 80.0, 90.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 321.673, 97.91, 120.0, 90.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 245.261, 203.08, 80.0, 90.0, 0.0] + ], + [ + [-0.63, 0.777, -0.777, -0.629, 397.32, 33.42, 320.0, 120.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 379.37, 31.75, 80.0, 90.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 322.731, 101.69, 120.0, 90.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 240.919, 202.72, 80.0, 90.0, 0.0] + ], + [ + [-0.67, 0.743, -0.743, -0.669, 401.65, 41.24, 320.0, 120.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 383.811, 38.64, 80.0, 90.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 323.589, 105.52, 120.0, 90.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 236.602, 202.13, 80.0, 90.0, 0.0] + ], + [ + [-0.71, 0.707, -0.707, -0.707, 405.563, 49.29, 320.0, 120.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 387.886, 45.75, 80.0, 90.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 324.246, 109.39, 120.0, 90.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 232.322, 201.32, 80.0, 90.0, 0.0] + ], + [ + [-0.74, 0.669, -0.669, -0.743, 409.051, 57.53, 320.0, 120.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 391.583, 53.07, 80.0, 90.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 324.7, 113.29, 120.0, 90.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 228.091, 200.28, 80.0, 90.0, 0.0] + ], + [ + [-0.78, 0.629, -0.629, -0.777, 412.103, 65.94, 320.0, 120.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 394.891, 60.57, 80.0, 90.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 324.948, 117.21, 120.0, 90.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 223.919, 199.02, 80.0, 90.0, 0.0] + ], + [ + [-0.81, 0.588, -0.588, -0.809, 414.71, 74.5, 320.0, 120.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 397.803, 68.24, 80.0, 90.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 324.991, 121.14, 120.0, 90.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 219.819, 197.55, 80.0, 90.0, 0.0] + ], + [ + [-0.84, 0.545, -0.545, -0.839, 416.866, 83.18, 320.0, 120.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 400.309, 76.04, 80.0, 90.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 324.829, 125.06, 120.0, 90.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 215.802, 195.86, 80.0, 90.0, 0.0] + ], + [ + [-0.87, 0.5, -0.5, -0.866, 418.564, 91.96, 320.0, 120.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 402.404, 83.97, 80.0, 90.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 324.462, 128.97, 120.0, 90.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 211.878, 193.97, 80.0, 90.0, 0.0] + ], + [ + [-0.89, 0.454, -0.454, -0.891, 419.8, 100.82, 320.0, 120.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 404.081, 92.0, 80.0, 90.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 323.89, 132.86, 120.0, 90.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 208.059, 191.87, 80.0, 90.0, 0.0] + ], + [ + [-0.91, 0.407, -0.407, -0.914, 420.571, 109.73, 320.0, 120.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 405.335, 100.1, 80.0, 90.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 323.116, 136.71, 120.0, 90.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 204.355, 189.58, 80.0, 90.0, 0.0] + ], + [ + [-0.93, 0.358, -0.358, -0.934, 420.875, 118.68, 320.0, 120.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 406.164, 108.26, 80.0, 90.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 322.141, 140.51, 120.0, 90.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 200.776, 187.1, 80.0, 90.0, 0.0] + ], + [ + [-0.95, 0.309, -0.309, -0.951, 420.71, 127.62, 320.0, 120.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 406.564, 116.44, 80.0, 90.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 320.969, 144.26, 120.0, 90.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 197.332, 184.43, 80.0, 90.0, 0.0] + ], + [ + [-0.97, 0.259, -0.259, -0.966, 420.077, 136.54, 320.0, 120.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 406.536, 124.64, 80.0, 90.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 319.602, 147.94, 120.0, 90.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 194.032, 181.58, 80.0, 90.0, 0.0] + ], + [ + [-0.98, 0.208, -0.208, -0.978, 418.978, 145.42, 320.0, 120.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 406.078, 132.83, 80.0, 90.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 318.045, 151.54, 120.0, 90.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 190.886, 178.57, 80.0, 90.0, 0.0] + ], + [ + [-0.99, 0.156, -0.156, -0.988, 417.416, 154.23, 320.0, 120.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 405.193, 140.98, 80.0, 90.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 316.301, 155.06, 120.0, 90.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 187.901, 175.4, 80.0, 90.0, 0.0] + ], + [ + [-0.99, 0.105, -0.105, -0.995, 415.395, 162.95, 320.0, 120.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 403.882, 149.07, 80.0, 90.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 314.375, 158.48, 120.0, 90.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 185.087, 172.07, 80.0, 90.0, 0.0] + ], + [ + [-1.0, 0.052, -0.052, -0.999, 412.921, 171.54, 320.0, 120.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 402.15, 157.09, 80.0, 90.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 312.273, 161.8, 120.0, 90.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 182.451, 168.6, 80.0, 90.0, 0.0] + ], + [ + [-1.0, 0.0, -0.0, -1.0, 410.0, 180.0, 320.0, 120.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 400.0, 165.0, 80.0, 90.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 310.0, 165.0, 120.0, 90.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 180.0, 165.0, 80.0, 90.0, 0.0] + ] + ] + }, + { + "id": "group", + "name": "group rotating in flow", + "labels": ["container", "a", "group.a", "group.b", "c"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 240.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 70.0, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 130.0, 110.0, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 244.6, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 104.71, 67.97, 50.0, 50.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 132.576, 109.48, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 194.601, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 248.97, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 109.408, 66.07, 50.0, 50.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 135.062, 108.98, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 198.969, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 253.09, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 114.079, 64.3, 50.0, 50.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 137.452, 108.5, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 203.094, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 256.96, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 118.712, 62.67, 50.0, 50.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 139.74, 108.03, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 206.964, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 260.57, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 123.294, 61.18, 50.0, 50.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 141.919, 107.58, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 210.568, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 263.9, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 127.812, 59.84, 50.0, 50.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 143.983, 107.15, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 213.896, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 266.94, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 132.253, 58.65, 50.0, 50.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 145.926, 106.75, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 216.94, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 269.69, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 136.606, 57.62, 50.0, 50.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 147.743, 106.36, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 219.69, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 272.14, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 140.859, 56.75, 50.0, 50.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 149.43, 106.01, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.14, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.28, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 145.0, 56.03, 50.0, 50.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 150.981, 105.67, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.282, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.11, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 149.018, 55.47, 50.0, 50.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 152.392, 105.36, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.111, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 277.62, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 152.901, 55.08, 50.0, 50.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 153.66, 105.08, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 227.622, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 278.81, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 156.639, 54.86, 50.0, 50.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 154.78, 104.82, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 228.811, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 279.67, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 160.222, 54.79, 50.0, 50.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 155.751, 104.59, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 229.673, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 280.21, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 163.64, 54.9, 50.0, 50.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 156.569, 104.39, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.208, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 280.41, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 166.883, 55.16, 50.0, 50.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 157.231, 104.22, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.413, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 280.29, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 169.943, 55.59, 50.0, 50.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 157.737, 104.08, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.289, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 279.83, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 172.812, 56.19, 50.0, 50.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 158.084, 103.97, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 229.834, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 279.05, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 175.48, 56.94, 50.0, 50.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 158.273, 103.89, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 229.051, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 277.94, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 177.942, 57.86, 50.0, 50.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 158.301, 103.84, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 227.942, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.51, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 180.191, 58.93, 50.0, 50.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 158.17, 103.82, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.51, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.76, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 182.219, 60.16, 50.0, 50.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 157.879, 103.83, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.758, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 272.69, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 184.022, 61.53, 50.0, 50.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 157.43, 103.87, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.692, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 270.32, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 185.595, 63.05, 50.0, 50.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 156.823, 103.94, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 220.316, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 267.64, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 186.933, 64.72, 50.0, 50.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 156.061, 104.05, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 217.639, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 264.67, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 188.033, 66.52, 50.0, 50.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 155.145, 104.18, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 214.666, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 261.41, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 188.892, 68.45, 50.0, 50.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 154.077, 104.34, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 211.407, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 257.87, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 189.507, 70.52, 50.0, 50.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 152.862, 104.53, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 207.869, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 254.06, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 189.877, 72.7, 50.0, 50.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 151.502, 104.75, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 204.064, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 250.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 190.0, 75.0, 50.0, 50.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 150.0, 105.0, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 200.0, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 254.06, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 194.064, 77.41, 50.0, 50.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 152.548, 105.28, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 204.064, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 257.87, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 197.869, 79.92, 50.0, 50.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 154.953, 105.58, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 207.869, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 261.41, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 201.407, 82.53, 50.0, 50.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 157.206, 105.91, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 211.407, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 264.67, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 204.666, 85.23, 50.0, 50.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 159.303, 106.26, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 214.666, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 267.64, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 207.639, 88.01, 50.0, 50.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 161.237, 106.63, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 217.639, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 270.32, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 210.316, 90.86, 50.0, 50.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 163.004, 107.03, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 220.316, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 272.69, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 212.692, 93.78, 50.0, 50.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 164.597, 107.46, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.692, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.76, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 214.758, 96.76, 50.0, 50.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 166.014, 107.9, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.758, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.51, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 216.51, 99.79, 50.0, 50.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 167.25, 108.36, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.51, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 277.94, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 217.942, 102.86, 50.0, 50.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 168.301, 108.84, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 227.942, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 279.05, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 219.051, 105.96, 50.0, 50.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 169.165, 109.34, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 229.051, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 279.83, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 219.834, 109.09, 50.0, 50.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 169.84, 109.85, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 229.834, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 280.29, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 220.289, 112.23, 50.0, 50.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 170.323, 110.38, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.289, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 280.41, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 220.413, 115.39, 50.0, 50.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 170.614, 110.91, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.413, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 280.21, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 220.208, 118.54, 50.0, 50.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 170.711, 111.46, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.208, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 279.67, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 219.673, 121.68, 50.0, 50.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 170.614, 112.02, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 229.673, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 278.81, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 218.811, 124.8, 50.0, 50.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 170.323, 112.59, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 228.811, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 277.62, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 217.622, 127.89, 50.0, 50.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 169.84, 113.17, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 227.622, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 276.11, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 216.111, 130.95, 50.0, 50.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 169.165, 113.75, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.111, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 274.28, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 214.282, 133.97, 50.0, 50.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 168.301, 114.33, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.282, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 272.14, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 212.14, 136.94, 50.0, 50.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 167.25, 114.92, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.14, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 269.69, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 209.69, 139.84, 50.0, 50.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 166.014, 115.5, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 219.69, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 266.94, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 206.94, 142.68, 50.0, 50.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 164.597, 116.08, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 216.94, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 263.9, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 203.896, 145.44, 50.0, 50.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 163.004, 116.67, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 213.896, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 260.57, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 200.568, 148.11, 50.0, 50.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 161.237, 117.24, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 210.568, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 256.96, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 196.964, 150.7, 50.0, 50.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 159.303, 117.81, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 206.964, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 253.09, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 193.094, 153.19, 50.0, 50.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 157.206, 118.37, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 203.094, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 248.97, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 188.969, 155.57, 50.0, 50.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 154.952, 118.93, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 198.969, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 244.6, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 184.601, 157.84, 50.0, 50.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 152.548, 119.47, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 194.601, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 240.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 180.0, 160.0, 50.0, 50.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 150.0, 120.0, 50.0, 50.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 65.0, 60.0, 100.0, 0.0] + ] + ] + }, + { + "id": "two", + "name": "two rotating siblings", + "labels": ["container", "rot", "still", "rot(-)"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 65.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 170.0, 65.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 230.3, 190.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 35.234, 63.5, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 105.151, 65.0, 60.0, 100.0, 0.0], + [1.0, -0.052, 0.052, 0.999, 175.151, 66.64, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 240.25, 190.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 40.453, 62.14, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 110.124, 65.0, 60.0, 100.0, 0.0], + [0.99, -0.105, 0.105, 0.995, 180.124, 68.41, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 249.81, 190.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 45.643, 60.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 114.905, 65.0, 60.0, 100.0, 0.0], + [0.99, -0.156, 0.156, 0.988, 184.905, 70.31, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 258.96, 190.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 50.791, 59.86, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 119.48, 65.0, 60.0, 100.0, 0.0], + [0.98, -0.208, 0.208, 0.978, 189.48, 72.33, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 267.67, 190.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 55.882, 58.94, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 123.837, 65.0, 60.0, 100.0, 0.0], + [0.97, -0.259, 0.259, 0.966, 193.837, 74.47, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 275.93, 190.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 60.902, 58.18, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 127.965, 65.0, 60.0, 100.0, 0.0], + [0.95, -0.309, 0.309, 0.951, 197.965, 76.72, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 283.7, 190.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 65.837, 57.57, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 131.852, 65.0, 60.0, 100.0, 0.0], + [0.93, -0.358, 0.358, 0.934, 201.852, 79.07, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 290.97, 190.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 70.674, 57.12, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 135.486, 65.0, 60.0, 100.0, 0.0], + [0.91, -0.407, 0.407, 0.914, 205.486, 81.52, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 297.72, 190.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 75.399, 56.83, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 138.859, 65.0, 60.0, 100.0, 0.0], + [0.89, -0.454, 0.454, 0.891, 208.859, 84.07, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 303.92, 190.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 80.0, 56.7, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 141.962, 65.0, 60.0, 100.0, 0.0], + [0.87, -0.5, 0.5, 0.866, 211.962, 86.7, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 309.57, 190.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 84.464, 56.73, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 144.784, 65.0, 60.0, 100.0, 0.0], + [0.84, -0.545, 0.545, 0.839, 214.784, 89.41, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 314.64, 190.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 88.779, 56.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 147.32, 65.0, 60.0, 100.0, 0.0], + [0.81, -0.588, 0.588, 0.809, 217.32, 92.18, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 319.12, 190.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 92.932, 57.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 149.561, 65.0, 60.0, 100.0, 0.0], + [0.78, -0.629, 0.629, 0.777, 219.561, 95.02, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 323.0, 190.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 96.913, 57.77, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 151.502, 65.0, 60.0, 100.0, 0.0], + [0.74, -0.669, 0.669, 0.743, 221.502, 97.92, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 326.27, 190.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 100.711, 58.43, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 153.137, 65.0, 60.0, 100.0, 0.0], + [0.71, -0.707, 0.707, 0.707, 223.137, 100.86, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 328.92, 190.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 104.314, 59.25, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 154.462, 65.0, 60.0, 100.0, 0.0], + [0.67, -0.743, 0.743, 0.669, 224.462, 103.84, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 330.95, 190.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 107.715, 60.22, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 155.474, 65.0, 60.0, 100.0, 0.0], + [0.63, -0.777, 0.777, 0.629, 225.474, 106.85, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 332.34, 190.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 110.902, 61.34, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.169, 65.0, 60.0, 100.0, 0.0], + [0.59, -0.809, 0.809, 0.588, 226.169, 109.88, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 333.09, 190.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 113.867, 62.61, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.545, 65.0, 60.0, 100.0, 0.0], + [0.54, -0.839, 0.839, 0.545, 226.545, 112.93, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 333.21, 190.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 116.603, 64.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.603, 65.0, 60.0, 100.0, 0.0], + [0.5, -0.866, 0.866, 0.5, 226.603, 115.98, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 332.68, 190.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 119.101, 65.57, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.34, 65.0, 60.0, 100.0, 0.0], + [0.45, -0.891, 0.891, 0.454, 226.34, 119.03, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 331.52, 190.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 121.355, 67.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 155.759, 65.0, 60.0, 100.0, 0.0], + [0.41, -0.914, 0.914, 0.407, 225.759, 122.07, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 329.72, 190.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 123.358, 69.07, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 154.86, 65.0, 60.0, 100.0, 0.0], + [0.36, -0.934, 0.934, 0.358, 224.86, 125.09, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 327.29, 190.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 125.106, 71.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 153.647, 65.0, 60.0, 100.0, 0.0], + [0.31, -0.951, 0.951, 0.309, 223.647, 128.08, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 324.24, 190.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 126.593, 73.08, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 152.122, 65.0, 60.0, 100.0, 0.0], + [0.26, -0.966, 0.966, 0.259, 222.122, 131.04, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 320.58, 190.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 127.815, 75.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 150.289, 65.0, 60.0, 100.0, 0.0], + [0.21, -0.978, 0.978, 0.208, 220.289, 133.95, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 316.31, 190.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 128.769, 77.55, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 148.155, 65.0, 60.0, 100.0, 0.0], + [0.16, -0.988, 0.988, 0.156, 218.155, 136.81, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 311.45, 190.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 129.452, 79.94, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 145.724, 65.0, 60.0, 100.0, 0.0], + [0.1, -0.995, 0.995, 0.105, 215.724, 139.61, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 306.01, 190.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 129.863, 82.42, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 143.003, 65.0, 60.0, 100.0, 0.0], + [0.05, -0.999, 0.999, 0.052, 213.003, 142.34, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 300.0, 190.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 130.0, 85.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 140.0, 65.0, 60.0, 100.0, 0.0], + [0.0, -1.0, 1.0, 0.0, 210.0, 145.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 306.01, 190.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 133.003, 87.66, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 143.003, 65.0, 60.0, 100.0, 0.0], + [-0.05, -0.999, 0.999, -0.052, 216.143, 147.58, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 311.45, 190.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 135.724, 90.39, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 145.724, 65.0, 60.0, 100.0, 0.0], + [-0.1, -0.995, 0.995, -0.105, 221.996, 150.06, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 316.31, 190.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 138.155, 93.19, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 148.155, 65.0, 60.0, 100.0, 0.0], + [-0.16, -0.988, 0.988, -0.156, 227.541, 152.45, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 320.58, 190.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 140.289, 96.05, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 150.289, 65.0, 60.0, 100.0, 0.0], + [-0.21, -0.978, 0.978, -0.208, 232.764, 154.74, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 324.24, 190.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 142.122, 98.96, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 152.122, 65.0, 60.0, 100.0, 0.0], + [-0.26, -0.966, 0.966, -0.259, 237.651, 156.92, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 327.29, 190.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 143.647, 101.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 153.647, 65.0, 60.0, 100.0, 0.0], + [-0.31, -0.951, 0.951, -0.309, 242.188, 158.98, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 329.72, 190.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 144.86, 104.91, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 154.86, 65.0, 60.0, 100.0, 0.0], + [-0.36, -0.934, 0.934, -0.358, 246.362, 160.93, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 331.52, 190.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 145.759, 107.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 155.759, 65.0, 60.0, 100.0, 0.0], + [-0.41, -0.914, 0.914, -0.407, 250.163, 162.74, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 332.68, 190.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 146.34, 110.97, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.34, 65.0, 60.0, 100.0, 0.0], + [-0.45, -0.891, 0.891, -0.454, 253.58, 164.43, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 333.21, 190.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 146.603, 114.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.603, 65.0, 60.0, 100.0, 0.0], + [-0.5, -0.866, 0.866, -0.5, 256.603, 165.98, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 333.09, 190.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 146.545, 117.07, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.545, 65.0, 60.0, 100.0, 0.0], + [-0.54, -0.839, 0.839, -0.545, 259.224, 167.39, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 332.34, 190.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 146.169, 120.12, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 156.169, 65.0, 60.0, 100.0, 0.0], + [-0.59, -0.809, 0.809, -0.588, 261.436, 168.66, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 330.95, 190.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 145.474, 123.15, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 155.474, 65.0, 60.0, 100.0, 0.0], + [-0.63, -0.777, 0.777, -0.629, 263.233, 169.78, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 328.92, 190.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 144.462, 126.16, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 154.462, 65.0, 60.0, 100.0, 0.0], + [-0.67, -0.743, 0.743, -0.669, 264.61, 170.75, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 326.27, 190.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 143.137, 129.14, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 153.137, 65.0, 60.0, 100.0, 0.0], + [-0.71, -0.707, 0.707, -0.707, 265.563, 171.57, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 323.0, 190.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 141.502, 132.08, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 151.502, 65.0, 60.0, 100.0, 0.0], + [-0.74, -0.669, 0.669, -0.743, 266.09, 172.23, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 319.12, 190.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 139.561, 134.98, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 149.561, 65.0, 60.0, 100.0, 0.0], + [-0.78, -0.629, 0.629, -0.777, 266.19, 172.74, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 314.64, 190.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 137.32, 137.82, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 147.32, 65.0, 60.0, 100.0, 0.0], + [-0.81, -0.588, 0.588, -0.809, 265.861, 173.08, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 309.57, 190.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 134.784, 140.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 144.784, 65.0, 60.0, 100.0, 0.0], + [-0.84, -0.545, 0.545, -0.839, 265.104, 173.27, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 303.92, 190.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 131.962, 143.3, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 141.962, 65.0, 60.0, 100.0, 0.0], + [-0.87, -0.5, 0.5, -0.866, 263.923, 173.3, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 297.72, 190.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 128.859, 145.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 138.859, 65.0, 60.0, 100.0, 0.0], + [-0.89, -0.454, 0.454, -0.891, 262.32, 173.17, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 290.97, 190.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 125.486, 148.48, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 135.486, 65.0, 60.0, 100.0, 0.0], + [-0.91, -0.407, 0.407, -0.914, 260.299, 172.88, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 283.7, 190.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 121.852, 150.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 131.852, 65.0, 60.0, 100.0, 0.0], + [-0.93, -0.358, 0.358, -0.934, 257.866, 172.43, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 275.93, 190.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 117.965, 153.28, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 127.965, 65.0, 60.0, 100.0, 0.0], + [-0.95, -0.309, 0.309, -0.951, 255.028, 171.82, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 267.67, 190.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 113.837, 155.53, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 123.837, 65.0, 60.0, 100.0, 0.0], + [-0.97, -0.259, 0.259, -0.966, 251.793, 171.06, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 258.96, 190.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 109.48, 157.67, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 119.48, 65.0, 60.0, 100.0, 0.0], + [-0.98, -0.208, 0.208, -0.978, 248.169, 170.14, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 249.81, 190.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 104.905, 159.69, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 114.905, 65.0, 60.0, 100.0, 0.0], + [-0.99, -0.156, 0.156, -0.988, 244.166, 169.08, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 240.25, 190.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 100.124, 161.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 110.124, 65.0, 60.0, 100.0, 0.0], + [-0.99, -0.105, 0.105, -0.995, 239.795, 167.86, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 230.3, 190.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 95.151, 163.36, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 105.151, 65.0, 60.0, 100.0, 0.0], + [-1.0, -0.052, 0.052, -0.999, 235.069, 166.5, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 220.0, 190.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 90.0, 165.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 100.0, 65.0, 60.0, 100.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 230.0, 165.0, 60.0, 100.0, 0.0] + ] + ] + }, + { + "id": "between", + "name": "space-between × rotation", + "labels": ["container", "a", "rot", "c"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 220.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 222.658, 53.5, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 225.391, 52.14, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 228.191, 50.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 231.051, 49.86, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 233.963, 48.94, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 236.919, 48.18, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 239.911, 47.57, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 242.93, 47.12, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 245.969, 46.83, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 249.019, 46.7, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 252.072, 46.73, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 255.119, 46.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 258.152, 47.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 261.162, 47.77, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 264.142, 48.43, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 267.083, 49.25, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 269.978, 50.22, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 272.817, 51.34, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 275.594, 52.61, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 278.301, 54.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 280.931, 55.57, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 283.475, 57.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 285.928, 59.07, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 288.282, 61.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 290.532, 63.08, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 292.67, 65.26, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 294.691, 67.55, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 296.59, 69.94, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 298.361, 72.42, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 300.0, 75.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 301.502, 77.66, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 302.862, 80.39, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 304.077, 83.19, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 305.145, 86.05, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 306.061, 88.96, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 306.823, 91.92, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 307.43, 94.91, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 307.879, 97.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 308.17, 100.97, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 308.301, 104.02, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 308.273, 107.07, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 308.084, 110.12, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 307.737, 113.15, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 307.231, 116.16, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 306.569, 119.14, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 305.751, 122.08, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 304.78, 124.98, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 303.66, 127.82, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 302.392, 130.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 300.981, 133.3, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 299.43, 135.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 297.743, 138.48, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 295.926, 140.93, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 293.983, 143.28, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 291.919, 145.53, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 289.74, 147.67, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 287.452, 149.69, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 285.062, 151.59, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 282.576, 153.36, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 460.0, 170.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 55.0, 60.0, 100.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 280.0, 155.0, 60.0, 100.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 410.0, 55.0, 60.0, 100.0, 0.0] + ] + ] + }, + { + "id": "wrap", + "name": "wrap reflow mid-rotation", + "labels": ["container", "a", "rot", "c", "d"], + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 110.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 143.5, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.052, -0.052, 0.999, 116.28, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 196.184, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 276.184, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 146.66, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.99, 0.105, -0.105, 0.995, 122.543, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 202.16, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 282.16, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 149.47, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.99, 0.156, -0.156, 0.988, 128.772, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 207.91, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 287.91, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 151.93, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.98, 0.208, -0.208, 0.978, 134.949, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 213.42, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 293.42, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 154.03, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.97, 0.259, -0.259, 0.966, 141.058, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 218.673, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 298.673, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 155.76, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.95, 0.309, -0.309, 0.951, 147.082, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 223.656, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 303.656, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 157.12, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.93, 0.358, -0.358, 0.934, 153.004, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 228.355, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 308.355, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.1, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.91, 0.407, -0.407, 0.914, 158.808, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 232.757, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 312.757, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.7, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.89, 0.454, -0.454, 0.891, 164.479, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 236.849, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 316.849, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.92, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.87, 0.5, -0.5, 0.866, 170.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 240.622, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.92, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.77, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.84, 0.545, -0.545, 0.839, 175.357, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 244.064, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.77, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.23, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.81, 0.588, -0.588, 0.809, 180.534, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 247.165, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.23, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 287.31, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.78, 0.629, -0.629, 0.777, 185.518, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 249.919, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 177.31, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 286.02, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.74, 0.669, -0.669, 0.743, 190.296, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 252.316, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 176.02, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 284.35, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.71, 0.707, -0.707, 0.707, 194.853, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 254.35, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 174.35, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 282.32, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.67, 0.743, -0.743, 0.669, 199.177, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 256.017, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 172.32, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 279.92, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.63, 0.777, -0.777, 0.629, 203.258, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 257.31, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 169.92, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 277.17, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.59, 0.809, -0.809, 0.588, 207.082, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.227, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 167.17, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 274.06, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.54, 0.839, -0.839, 0.545, 210.64, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.765, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 164.06, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.62, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.5, 0.866, -0.866, 0.5, 213.923, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.923, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.62, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.45, 0.891, -0.891, 0.454, 216.921, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.7, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.41, 0.914, -0.914, 0.407, 219.625, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.097, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.36, 0.934, -0.934, 0.358, 222.03, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 257.115, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.31, 0.951, -0.951, 0.309, 224.127, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 255.758, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.26, 0.966, -0.966, 0.259, 225.911, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 254.028, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.21, 0.978, -0.978, 0.208, 227.378, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 251.932, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.16, 0.988, -0.988, 0.156, 228.523, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 249.473, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.1, 0.995, -0.995, 0.105, 229.343, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 246.66, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.05, 0.999, -0.999, 0.052, 229.836, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 243.499, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [0.0, 1.0, -1.0, 0.0, 230.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 240.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 320.0, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.05, 0.999, -0.999, -0.052, 233.499, 36.28, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 243.499, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.1, 0.995, -0.995, -0.105, 236.66, 42.54, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 246.66, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.16, 0.988, -0.988, -0.156, 239.473, 48.77, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 249.473, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.21, 0.978, -0.978, -0.208, 241.932, 54.95, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 251.932, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.26, 0.966, -0.966, -0.259, 244.028, 61.06, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 254.028, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.31, 0.951, -0.951, -0.309, 245.758, 67.08, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 255.758, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.36, 0.934, -0.934, -0.358, 247.115, 73.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 257.115, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.41, 0.914, -0.914, -0.407, 248.097, 78.81, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.097, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.45, 0.891, -0.891, -0.454, 248.7, 84.48, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.7, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 270.62, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.5, 0.866, -0.866, -0.5, 248.923, 90.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.923, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 160.62, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 274.06, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.54, 0.839, -0.839, -0.545, 248.765, 95.36, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.765, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 164.06, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 277.17, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.59, 0.809, -0.809, -0.588, 248.227, 100.53, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 258.227, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 167.17, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 279.92, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.63, 0.777, -0.777, -0.629, 247.31, 105.52, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 257.31, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 169.92, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 282.32, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.67, 0.743, -0.743, -0.669, 246.017, 110.3, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 256.017, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 172.32, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 284.35, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.71, 0.707, -0.707, -0.707, 244.35, 114.85, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 254.35, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 174.35, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 286.02, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.74, 0.669, -0.669, -0.743, 242.316, 119.18, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 252.316, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 176.02, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 287.31, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.78, 0.629, -0.629, -0.777, 239.919, 123.26, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 249.919, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 177.31, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.23, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.81, 0.588, -0.588, -0.809, 237.165, 127.08, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 247.165, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.23, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.77, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.84, 0.545, -0.545, -0.839, 234.064, 130.64, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 244.064, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.77, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 288.92, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.87, 0.5, -0.5, -0.866, 230.622, 133.92, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 240.622, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 178.92, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.7, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.89, 0.454, -0.454, -0.891, 226.849, 136.92, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 236.849, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 316.849, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 158.1, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.91, 0.407, -0.407, -0.914, 222.757, 139.63, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 232.757, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 312.757, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 157.12, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.93, 0.358, -0.358, -0.934, 218.355, 142.03, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 228.355, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 308.355, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 155.76, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.95, 0.309, -0.309, -0.951, 213.656, 144.13, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 223.656, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 303.656, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 154.03, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.97, 0.259, -0.259, -0.966, 208.673, 145.91, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 218.673, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 298.673, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 151.93, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.98, 0.208, -0.208, -0.978, 203.42, 147.38, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 213.42, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 293.42, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 149.47, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.99, 0.156, -0.156, -0.988, 197.91, 148.52, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 207.91, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 287.91, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 146.66, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-0.99, 0.105, -0.105, -0.995, 192.16, 149.34, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 202.16, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 282.16, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 143.5, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-1.0, 0.052, -0.052, -0.999, 186.184, 149.84, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 196.184, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 276.184, 30.0, 70.0, 120.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 20.0, 380.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 30.0, 30.0, 70.0, 120.0, 0.0], + [-1.0, 0.0, -0.0, -1.0, 180.0, 150.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 30.0, 70.0, 120.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 270.0, 30.0, 70.0, 120.0, 0.0] + ] + ] + }, + { + "id": "flipzero", + "name": "resize across zero (flip)", + "labels": ["wall", "R", "slide", "R", "flip", "R"], + "outlines": [0, 2, 4], + "marker": true, + "sweep": { "label": "drag", "from": 60, "to": -60, "unit": "px" }, + "frames": [ + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 60.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 60.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 60.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 58.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 58.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 58.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 56.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 56.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 56.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 54.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 54.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 54.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 52.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 52.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 52.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 50.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 50.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 50.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 48.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 48.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 48.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 46.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 46.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 46.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 44.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 44.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 44.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 42.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 42.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 42.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 40.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 40.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 40.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 38.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 38.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 38.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 36.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 36.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 36.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 34.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 34.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 34.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 32.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 32.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 32.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 30.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 30.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 30.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 28.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 28.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 28.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 26.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 26.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 26.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 24.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 24.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 24.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 22.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 22.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 22.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 20.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 20.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 20.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 18.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 18.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 18.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 16.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 16.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 16.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 14.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 14.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 14.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 12.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 12.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 12.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 10.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 10.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 10.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 8.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 8.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 8.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 6.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 6.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 6.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 4.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 4.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 4.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 2.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 2.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 2.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 250.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 0.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 248.0, 65.0, 2.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 2.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 246.0, 65.0, 4.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 4.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 244.0, 65.0, 6.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 6.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 242.0, 65.0, 8.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 8.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 240.0, 65.0, 10.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 10.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 238.0, 65.0, 12.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 12.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 236.0, 65.0, 14.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 14.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 234.0, 65.0, 16.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 16.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 232.0, 65.0, 18.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 18.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 230.0, 65.0, 20.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 20.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 228.0, 65.0, 22.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 22.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 226.0, 65.0, 24.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 24.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 224.0, 65.0, 26.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 26.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 222.0, 65.0, 28.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 28.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 220.0, 65.0, 30.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 30.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 218.0, 65.0, 32.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 32.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 216.0, 65.0, 34.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 34.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 214.0, 65.0, 36.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 36.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 212.0, 65.0, 38.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 38.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 210.0, 65.0, 40.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 40.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 208.0, 65.0, 42.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 42.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 206.0, 65.0, 44.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 44.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 204.0, 65.0, 46.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 46.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 202.0, 65.0, 48.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 48.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 200.0, 65.0, 50.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 50.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 198.0, 65.0, 52.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 52.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 196.0, 65.0, 54.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 54.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 194.0, 65.0, 56.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 56.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 192.0, 65.0, 58.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 58.0, 70.0, 0.0] + ], + [ + [1.0, 0.0, 0.0, 1.0, 20.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 95.0, 65.0, 0.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 175.0, 30.0, 145.0, 140.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 190.0, 65.0, 60.0, 70.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 330.0, 30.0, 145.0, 140.0, 0.0], + [-1.0, 0.0, 0.0, 1.0, 405.0, 65.0, 60.0, 70.0, 0.0] + ] + ] + } + ] +} diff --git a/archive/model-v2/anchor/edge-cases/index.html b/archive/model-v2/anchor/edge-cases/index.html new file mode 100644 index 00000000..698d2d5a --- /dev/null +++ b/archive/model-v2/anchor/edge-cases/index.html @@ -0,0 +1,4053 @@ +anchor — rotation × layout edge cases + + +
+
+
+ model-v2 · anchor · edge cases — 2026-07-07 +
+

Rotation × layout, at the edges.

+

+ Nine scenes where the model’s laws intersect, + resolved by the actual Rust lab (every frame is engine output, not + a mockup). The question per scene: does it read naturally? The + discontinuity meter is the objective proxy — the biggest single-step + jump of any box through the whole sweep. Two scenes fail it; they are now + findings E-A11/E-A12, not footnotes. The ninth scene sweeps a drag, + not an angle — the cross-zero resize decision (DEC-9), three + policies side by side. +

+
+ +
+ +
+
+ θ = 0° + + +
+ +
+ container + biggest single-step box jump this sweep + + +
+
+
+
+ + diff --git a/archive/model-v2/anchor/format-roundtrip/README.md b/archive/model-v2/anchor/format-roundtrip/README.md new file mode 100644 index 00000000..c11f61bd --- /dev/null +++ b/archive/model-v2/anchor/format-roundtrip/README.md @@ -0,0 +1,52 @@ +# E2 — anchor.fbs draft + codec round-trip + +**Question.** Can the anchor header — per-axis `AxisBinding` unions, +`SizeIntent` unions, nullable min/max boxes, payload union — encode under +the grida.fbs header rules (H9: tables-over-structs, additive evolution; +H11: unset is structural, never a scalar sentinel), and does the codec +round-trip? + +**Method.** [`anchor.fbs`](./anchor.fbs) is the standalone schema draft of +models/anchor.md §9 (geometry header full-fidelity; identity/paint stubbed). +[`quartet.json`](./quartet.json) encodes the a.md §7 worked examples plus +sentinel-freedom witnesses (`SizeAuto` vs absent, `max_lines` box, +aspect-ratio table, lens op vector). No generated code needed — +`flatc` (25.12.19) itself is the codec: JSON → binary → JSON → binary. +Reproduce with [`run.sh`](./run.sh). + +**Results (2026-07-07).** All pass: + +- **schema compiles** (`flatc --binary --schema`), v1 and v2. +- **S-1 fixpoint**: encode → decode → re-encode is **byte-identical**. +- **S-2**: f32 values (15.0, 0.25, 3.5, 16:9) exact through the trip. +- **H11 witnessed in the decoded JSON**: a node with no `x`/`y` keys + (⇒ `Pin{Start,0}` by rule) is structurally distinct from an explicit + `Span{0,0}`; `height_type: SizeAuto` (present, empty) is distinct from + absent height (⇒ kind default); absent `max_lines` box ⇒ unlimited — + the `max_lines: 0 ≡ unset` class of hack is unrepresentable. +- **M-4 forward**: v1 binary decodes under a v2 schema (new nullable + header field + new payload arm) — new fields read as absent. +- **M-4 backward**: v2 binary decodes under the v1 schema — unknown + fields skipped safely (vtables). + +**Findings** (into the phase-3 spec): + +1. **RMW-preservation is a policy, not a freebie.** FlatBuffers _reads_ + unknown fields safely, but a decode→re-encode through an old schema + **drops** them (verified: `corner_radius` vanishes from the v1 + projection). M-4's "preserves unknown content through + read-modify-write" needs an explicit mechanism: retain the original + buffer and patch, or version-gate writers to the newest schema. + Phase-3 must lock one. +2. **Scalar-default elision is aggressive and fine — but choose defaults + as semantics.** `parent.id: 0` and `root: 0` elide (default-valued); + absence-of-table vs present-table-with-defaults carries the real + distinction. Rule of thumb confirmed: _state lives in table presence; + scalar defaults must equal the semantic default_ — exactly how the + schema is drafted. +3. Union-vector lens ops (`[LensOpSlot]` wrapping `LensOpU`) round-trip + cleanly — the "each op parameter is an animation channel" encoding + works without codegen tricks. + +Verdict: **encodable, round-trips, evolves additively — pass**, with +finding 1 as the one genuine open policy for phase 3. diff --git a/archive/model-v2/anchor/format-roundtrip/anchor.fbs b/archive/model-v2/anchor/format-roundtrip/anchor.fbs new file mode 100644 index 00000000..4651399f --- /dev/null +++ b/archive/model-v2/anchor/format-roundtrip/anchor.fbs @@ -0,0 +1,134 @@ +// anchor.fbs — E2 draft of the anchor model encoding (models/a.md §9). +// +// Written against the grida.fbs header rules: +// - intent states are STRUCTURAL (unions / nullable tables), never scalar +// sentinels (H11): absent AxisBinding ⇒ Pin{Start,0}; absent SizeIntent +// ⇒ the kind default; absent MinMax ⇒ unconstrained; absent MaxLines ⇒ +// unlimited (the current `max_lines: 0 ≡ unset` hack is unrepresentable). +// - tables over structs everywhere evolution is conceivable. +// - evolution is additive: new payloads extend NodePayload; new binding +// forms extend AxisBinding (e.g. a future PinPercent), new layout modes +// extend LayoutBehavior. +// +// This is a lab-standalone schema: identity/asset/paint tables from the +// real grida.fbs are stubbed to keep E2 focused on the geometry header. + +namespace grida.anchor; + +// ---------- geometry intent ---------- + +enum AnchorEdge : byte { Start = 0, Center = 1, End = 2 } + +table Pin { anchor: AnchorEdge = Start; offset: float = 0; } +table Span { start: float = 0; end: float = 0; } +union AxisBinding { Pin, Span } // absent ⇒ Pin{Start, 0} + +table SizeFixed { value: float; } +table SizeAuto {} +union SizeIntent { SizeFixed, SizeAuto } // absent ⇒ kind default (a.md §4) + +// nullable scalar boxes — structural unset, no sentinel values +table FloatBox { value: float; } +table UInt32Box { value: uint32; } +table StringBox { value: string; } + +table AspectRatio { w: float; h: float; } + +enum Flow : byte { InFlow = 0, Absolute = 1 } +enum SelfAlign : byte { Auto = 0, Start = 1, Center = 2, End = 3, Stretch = 4 } + +// ---------- hierarchy ---------- + +table ParentRef { + id: uint32; + order: string; // fractional index (S-4) +} + +// ---------- the uniform header ---------- + +table NodeHeader { + id: uint32; + name: string; + active: bool = true; + locked: bool = false; + parent: ParentRef; // absent ⇒ scene root + + x: AxisBinding; + y: AxisBinding; + width: SizeIntent; + height: SizeIntent; + min_width: FloatBox; // nullable tables, not sentinels + max_width: FloatBox; + min_height: FloatBox; + max_height: FloatBox; + aspect_ratio: AspectRatio; // absent ⇒ unset (no (0,0) hack) + rotation: float = 0; // degrees; pivot per a.md §5 + + flow: Flow = InFlow; + grow: float = 0; + self_align: SelfAlign = Auto; + + opacity: float = 1; +} + +// ---------- payloads (lab subset; the full set is additive) ---------- + +enum LayoutMode : byte { None = 0, Flex = 1 } +enum Direction : byte { Row = 0, Column = 1 } +enum MainAlign : byte { Start = 0, Center = 1, End = 2, SpaceBetween = 3, SpaceAround = 4, SpaceEvenly = 5 } +enum CrossAlign : byte { Start = 0, Center = 1, End = 2, Stretch = 3 } + +table EdgeInsets { top: float = 0; right: float = 0; bottom: float = 0; left: float = 0; } + +table LayoutBehavior { + mode: LayoutMode = None; + direction: Direction = Row; + wrap: bool = false; + main_align: MainAlign = Start; + cross_align: CrossAlign = Start; + padding: EdgeInsets; + gap_main: float = 0; + gap_cross: float = 0; +} + +// shape descriptors are size-free; evaluated at the resolved box (a.md §3.2) +table RectDesc {} +table EllipseDesc { inner_ratio: float = 0; start_angle: float = 0; sweep: float = 360; } +table LineDesc {} +table RegularPolygonDesc { points: uint32; } +table StarDesc { points: uint32; inner_ratio: float; } +union ShapeDesc { RectDesc, EllipseDesc, LineDesc, RegularPolygonDesc, StarDesc } + +table FramePayload { layout: LayoutBehavior; clips_content: bool = false; } +table ShapePayload { desc: ShapeDesc; } +table TextPayload { + content: string; + font_size: float = 16; + max_lines: UInt32Box; // absent ⇒ unlimited (T-2 witness) + ellipsis: StringBox; // absent ⇒ "…"; empty string ⇒ none +} +table GroupPayload {} + +// each lens op parameter is a named scalar — an animation channel (a.md §3.3) +table TranslateOp { x: float = 0; y: float = 0; } +table RotateOp { deg: float = 0; } +table ScaleOp { x: float = 1; y: float = 1; } +table SkewOp { x_deg: float = 0; y_deg: float = 0; } +table Matrix2D { m00: float; m01: float; m02: float; m10: float; m11: float; m12: float; } +union LensOpU { TranslateOp, RotateOp, ScaleOp, SkewOp, Matrix2D } +table LensOpSlot { op: LensOpU; } +table LensPayload { ops: [LensOpSlot]; } + +union NodePayload { FramePayload, ShapePayload, TextPayload, GroupPayload, LensPayload } + +table NodeSlot { + header: NodeHeader (required); + payload: NodePayload; +} + +table AnchorDocument { + nodes: [NodeSlot]; + root: uint32; +} + +root_type AnchorDocument; diff --git a/archive/model-v2/anchor/format-roundtrip/anchor_v2.fbs b/archive/model-v2/anchor/format-roundtrip/anchor_v2.fbs new file mode 100644 index 00000000..4a9297a8 --- /dev/null +++ b/archive/model-v2/anchor/format-roundtrip/anchor_v2.fbs @@ -0,0 +1,136 @@ +// anchor.fbs — E2 draft of the anchor model encoding (models/a.md §9). +// +// Written against the grida.fbs header rules: +// - intent states are STRUCTURAL (unions / nullable tables), never scalar +// sentinels (H11): absent AxisBinding ⇒ Pin{Start,0}; absent SizeIntent +// ⇒ the kind default; absent MinMax ⇒ unconstrained; absent MaxLines ⇒ +// unlimited (the current `max_lines: 0 ≡ unset` hack is unrepresentable). +// - tables over structs everywhere evolution is conceivable. +// - evolution is additive: new payloads extend NodePayload; new binding +// forms extend AxisBinding (e.g. a future PinPercent), new layout modes +// extend LayoutBehavior. +// +// This is a lab-standalone schema: identity/asset/paint tables from the +// real grida.fbs are stubbed to keep E2 focused on the geometry header. + +namespace grida.anchor; + +// ---------- geometry intent ---------- + +enum AnchorEdge : byte { Start = 0, Center = 1, End = 2 } + +table Pin { anchor: AnchorEdge = Start; offset: float = 0; } +table Span { start: float = 0; end: float = 0; } +union AxisBinding { Pin, Span } // absent ⇒ Pin{Start, 0} + +table SizeFixed { value: float; } +table SizeAuto {} +union SizeIntent { SizeFixed, SizeAuto } // absent ⇒ kind default (a.md §4) + +// nullable scalar boxes — structural unset, no sentinel values +table FloatBox { value: float; } +table UInt32Box { value: uint32; } +table StringBox { value: string; } + +table AspectRatio { w: float; h: float; } + +enum Flow : byte { InFlow = 0, Absolute = 1 } +enum SelfAlign : byte { Auto = 0, Start = 1, Center = 2, End = 3, Stretch = 4 } + +// ---------- hierarchy ---------- + +table ParentRef { + id: uint32; + order: string; // fractional index (S-4) +} + +// ---------- the uniform header ---------- + +table NodeHeader { + id: uint32; + name: string; + active: bool = true; + locked: bool = false; + parent: ParentRef; // absent ⇒ scene root + + x: AxisBinding; + y: AxisBinding; + width: SizeIntent; + height: SizeIntent; + min_width: FloatBox; // nullable tables, not sentinels + max_width: FloatBox; + min_height: FloatBox; + max_height: FloatBox; + aspect_ratio: AspectRatio; // absent ⇒ unset (no (0,0) hack) + rotation: float = 0; // degrees; pivot per a.md §5 + + flow: Flow = InFlow; + grow: float = 0; + self_align: SelfAlign = Auto; + + opacity: float = 1; + corner_radius: FloatBox; // v2 additive field +} + +// ---------- payloads (lab subset; the full set is additive) ---------- + +enum LayoutMode : byte { None = 0, Flex = 1 } +enum Direction : byte { Row = 0, Column = 1 } +enum MainAlign : byte { Start = 0, Center = 1, End = 2, SpaceBetween = 3, SpaceAround = 4, SpaceEvenly = 5 } +enum CrossAlign : byte { Start = 0, Center = 1, End = 2, Stretch = 3 } + +table EdgeInsets { top: float = 0; right: float = 0; bottom: float = 0; left: float = 0; } + +table LayoutBehavior { + mode: LayoutMode = None; + direction: Direction = Row; + wrap: bool = false; + main_align: MainAlign = Start; + cross_align: CrossAlign = Start; + padding: EdgeInsets; + gap_main: float = 0; + gap_cross: float = 0; +} + +// shape descriptors are size-free; evaluated at the resolved box (a.md §3.2) +table RectDesc {} +table EllipseDesc { inner_ratio: float = 0; start_angle: float = 0; sweep: float = 360; } +table LineDesc {} +table RegularPolygonDesc { points: uint32; } +table StarDesc { points: uint32; inner_ratio: float; } +union ShapeDesc { RectDesc, EllipseDesc, LineDesc, RegularPolygonDesc, StarDesc } + +table FramePayload { layout: LayoutBehavior; clips_content: bool = false; } +table ShapePayload { desc: ShapeDesc; } +table TextPayload { + content: string; + font_size: float = 16; + max_lines: UInt32Box; // absent ⇒ unlimited (T-2 witness) + ellipsis: StringBox; // absent ⇒ "…"; empty string ⇒ none +} +table GroupPayload {} + +// each lens op parameter is a named scalar — an animation channel (a.md §3.3) +table TranslateOp { x: float = 0; y: float = 0; } +table RotateOp { deg: float = 0; } +table ScaleOp { x: float = 1; y: float = 1; } +table SkewOp { x_deg: float = 0; y_deg: float = 0; } +table Matrix2D { m00: float; m01: float; m02: float; m10: float; m11: float; m12: float; } +union LensOpU { TranslateOp, RotateOp, ScaleOp, SkewOp, Matrix2D } +table LensOpSlot { op: LensOpU; } +table LensPayload { ops: [LensOpSlot]; } + +table EmbedPayload { source: string; } +union NodePayload { FramePayload, ShapePayload, TextPayload, GroupPayload, LensPayload, EmbedPayload } + +table NodeSlot { + header: NodeHeader (required); + payload: NodePayload; +} + +table AnchorDocument { + nodes: [NodeSlot]; + root: uint32; +} + +root_type AnchorDocument; diff --git a/archive/model-v2/anchor/format-roundtrip/quartet.json b/archive/model-v2/anchor/format-roundtrip/quartet.json new file mode 100644 index 00000000..eb519dbc --- /dev/null +++ b/archive/model-v2/anchor/format-roundtrip/quartet.json @@ -0,0 +1,140 @@ +{ + "nodes": [ + { + "header": { + "id": 0, + "name": "root", + "x": { "start": 0.0, "end": 0.0 }, + "x_type": "Span", + "y": { "start": 0.0, "end": 0.0 }, + "y_type": "Span" + }, + "payload_type": "FramePayload", + "payload": {} + }, + { + "header": { + "id": 1, + "name": "a-rotated-rect", + "parent": { "id": 0, "order": "a0" }, + "x": { "offset": 10.0 }, + "x_type": "Pin", + "y": { "offset": 20.0 }, + "y_type": "Pin", + "width": { "value": 120.0 }, + "width_type": "SizeFixed", + "height": { "value": 80.0 }, + "height_type": "SizeFixed", + "rotation": 15.0 + }, + "payload_type": "ShapePayload", + "payload": { "desc_type": "RectDesc", "desc": {} } + }, + { + "header": { + "id": 2, + "name": "b-end-pinned", + "parent": { "id": 0, "order": "a1" }, + "x": { "anchor": "End", "offset": 24.0 }, + "x_type": "Pin", + "y": { "offset": 20.0 }, + "y_type": "Pin", + "width": { "value": 120.0 }, + "width_type": "SizeFixed", + "height": { "value": 80.0 }, + "height_type": "SizeFixed" + }, + "payload_type": "ShapePayload", + "payload": { "desc_type": "RectDesc", "desc": {} } + }, + { + "header": { + "id": 3, + "name": "c-flex-column", + "parent": { "id": 0, "order": "a2" }, + "width": { "value": 400.0 }, + "width_type": "SizeFixed", + "height": {}, + "height_type": "SizeAuto" + }, + "payload_type": "FramePayload", + "payload": { + "layout": { + "mode": "Flex", + "direction": "Column", + "gap_main": 8.0, + "padding": { + "top": 16.0, + "right": 16.0, + "bottom": 16.0, + "left": 16.0 + } + } + } + }, + { + "header": { + "id": 4, + "name": "stretch-rect", + "parent": { "id": 3, "order": "a0" }, + "width": { "value": 100.0 }, + "width_type": "SizeFixed", + "height": { "value": 40.0 }, + "height_type": "SizeFixed", + "self_align": "Stretch" + }, + "payload_type": "ShapePayload", + "payload": { "desc_type": "RectDesc", "desc": {} } + }, + { + "header": { + "id": 5, + "name": "grow-text", + "parent": { "id": 3, "order": "a1" }, + "grow": 1.0 + }, + "payload_type": "TextPayload", + "payload": { "content": "hello", "max_lines": { "value": 2 } } + }, + { + "header": { + "id": 6, + "name": "d-group", + "parent": { "id": 0, "order": "a3" }, + "x": { "offset": 100.0 }, + "x_type": "Pin", + "y": { "offset": 50.0 }, + "y_type": "Pin", + "rotation": 30.0 + }, + "payload_type": "GroupPayload", + "payload": {} + }, + { + "header": { + "id": 7, + "name": "e-lens", + "parent": { "id": 0, "order": "a4" }, + "aspect_ratio": { "w": 16.0, "h": 9.0 } + }, + "payload_type": "LensPayload", + "payload": { + "ops": [ + { "op_type": "SkewOp", "op": { "x_deg": 20.0 } }, + { + "op_type": "Matrix2D", + "op": { + "m00": 1.0, + "m01": 0.25, + "m02": 0.0, + "m10": 0.0, + "m11": 1.0, + "m12": 3.5 + } + } + ] + } + } + ], + "root": 0 +} diff --git a/archive/model-v2/anchor/format-roundtrip/quartet_v2.json b/archive/model-v2/anchor/format-roundtrip/quartet_v2.json new file mode 100644 index 00000000..af822d9c --- /dev/null +++ b/archive/model-v2/anchor/format-roundtrip/quartet_v2.json @@ -0,0 +1,141 @@ +{ + "nodes": [ + { + "header": { + "id": 0, + "name": "root", + "x": { "start": 0.0, "end": 0.0 }, + "x_type": "Span", + "y": { "start": 0.0, "end": 0.0 }, + "y_type": "Span" + }, + "payload_type": "FramePayload", + "payload": {} + }, + { + "header": { + "id": 1, + "name": "a-rotated-rect", + "parent": { "id": 0, "order": "a0" }, + "x": { "offset": 10.0 }, + "x_type": "Pin", + "y": { "offset": 20.0 }, + "y_type": "Pin", + "width": { "value": 120.0 }, + "width_type": "SizeFixed", + "height": { "value": 80.0 }, + "height_type": "SizeFixed", + "rotation": 15.0, + "corner_radius": { "value": 8.0 } + }, + "payload_type": "ShapePayload", + "payload": { "desc_type": "RectDesc", "desc": {} } + }, + { + "header": { + "id": 2, + "name": "b-end-pinned", + "parent": { "id": 0, "order": "a1" }, + "x": { "anchor": "End", "offset": 24.0 }, + "x_type": "Pin", + "y": { "offset": 20.0 }, + "y_type": "Pin", + "width": { "value": 120.0 }, + "width_type": "SizeFixed", + "height": { "value": 80.0 }, + "height_type": "SizeFixed" + }, + "payload_type": "ShapePayload", + "payload": { "desc_type": "RectDesc", "desc": {} } + }, + { + "header": { + "id": 3, + "name": "c-flex-column", + "parent": { "id": 0, "order": "a2" }, + "width": { "value": 400.0 }, + "width_type": "SizeFixed", + "height": {}, + "height_type": "SizeAuto" + }, + "payload_type": "FramePayload", + "payload": { + "layout": { + "mode": "Flex", + "direction": "Column", + "gap_main": 8.0, + "padding": { + "top": 16.0, + "right": 16.0, + "bottom": 16.0, + "left": 16.0 + } + } + } + }, + { + "header": { + "id": 4, + "name": "stretch-rect", + "parent": { "id": 3, "order": "a0" }, + "width": { "value": 100.0 }, + "width_type": "SizeFixed", + "height": { "value": 40.0 }, + "height_type": "SizeFixed", + "self_align": "Stretch" + }, + "payload_type": "ShapePayload", + "payload": { "desc_type": "RectDesc", "desc": {} } + }, + { + "header": { + "id": 5, + "name": "grow-text", + "parent": { "id": 3, "order": "a1" }, + "grow": 1.0 + }, + "payload_type": "TextPayload", + "payload": { "content": "hello", "max_lines": { "value": 2 } } + }, + { + "header": { + "id": 6, + "name": "d-group", + "parent": { "id": 0, "order": "a3" }, + "x": { "offset": 100.0 }, + "x_type": "Pin", + "y": { "offset": 50.0 }, + "y_type": "Pin", + "rotation": 30.0 + }, + "payload_type": "GroupPayload", + "payload": {} + }, + { + "header": { + "id": 7, + "name": "e-lens", + "parent": { "id": 0, "order": "a4" }, + "aspect_ratio": { "w": 16.0, "h": 9.0 } + }, + "payload_type": "LensPayload", + "payload": { + "ops": [ + { "op_type": "SkewOp", "op": { "x_deg": 20.0 } }, + { + "op_type": "Matrix2D", + "op": { + "m00": 1.0, + "m01": 0.25, + "m02": 0.0, + "m10": 0.0, + "m11": 1.0, + "m12": 3.5 + } + } + ] + } + } + ], + "root": 0 +} diff --git a/archive/model-v2/anchor/format-roundtrip/run.sh b/archive/model-v2/anchor/format-roundtrip/run.sh new file mode 100755 index 00000000..0e8b0657 --- /dev/null +++ b/archive/model-v2/anchor/format-roundtrip/run.sh @@ -0,0 +1,14 @@ +#!/bin/sh -e +# E2 reproduction: schema compile, S-1 byte-fixpoint, additive evolution. +flatc --binary --schema anchor.fbs +flatc --binary --schema anchor_v2.fbs +rm -rf rt rt2 rt_v2 rt_v2b && mkdir -p rt rt2 rt_v2 rt_v2b +flatc --binary -o rt anchor.fbs quartet.json +flatc --json --raw-binary --strict-json -o rt anchor.fbs -- rt/quartet.bin +flatc --binary -o rt2 anchor.fbs rt/quartet.json +cmp rt/quartet.bin rt2/quartet.bin && echo "S-1 FIXPOINT: byte-identical" +flatc --json --raw-binary --strict-json -o rt_v2 anchor_v2.fbs -- rt/quartet.bin +echo "M-4 forward: v1 binary decodes under v2 schema" +flatc --binary -o rt_v2b anchor_v2.fbs quartet_v2.json +flatc --json --raw-binary --strict-json -o rt_v2b anchor.fbs -- rt_v2b/quartet_v2.bin +echo "M-4 backward: v2 binary decodes under v1 schema (unknown field skipped)" diff --git a/archive/model-v2/anchor/free-editing/index.html b/archive/model-v2/anchor/free-editing/index.html new file mode 100644 index 00000000..8956352a --- /dev/null +++ b/archive/model-v2/anchor/free-editing/index.html @@ -0,0 +1,911 @@ +anchor — free context: the editing experience + + +
+
+
+ model-v2 · anchor · free context — 2026-07-07 +
+

Without layout: the base case, hands on.

+

+ A live free-context document — no layout engine runs here; + resolution is bindings + one rotation scalar, mirrored from the + lab’s free-context rules. Drag bodies to move, the corner dot to + resize, the stick to rotate. Watch two things: the + IR stays SVG-shaped, and every gesture is a + counted set of scalar writes (never a matrix). Then drag the + artboard’s right edge: the green badge is + end-pinned and the amber bar is spanned — they respond + with zero writes, because position is a relation. That is what SVG + cannot say, and it is the on-ramp to layout-as-feature. +

+
+ +
+
+

canvas — free frame, five nodes

+ +
+ click = select · drag = move (2 writes) · corner dot = + resize (2) · stick = rotate (1; group = 3 center-feel) · + right-edge grip = artboard resize (0 writes) +
+
+ select a node — reads come from the resolved tier +
+
+
+

the IR — canonical print, live

+

+      

writes — per gesture, typed

+
no gestures yet
+
+
+ +
+ what this shows + Free context is not a degraded mode — it is the model’s ground + floor. + Pin Start is literally svg x/y; rotation is one scalar with + one pivot rule (box center — so rotate is a 1-field write, no x/y + compensation, and resize-after-rotate never drifts: R-4 commutes, + lab-tested). A group is a named set with a coordinate space; dragging + it is 2 writes on the group, its children untouched. What SVG has no words + for: end / center / span bindings — responsiveness without any + layout engine, which is why “svg-first, layout-as-feature” is a + gradient here, not a mode switch: free (bindings) → constrained + (end/center/span) → flex (a frame feature). The lens (general + transform) never appears in the base case — it exists only when you + import one. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
gesturewritesfields
move2x.offset, y.offset (delta-form; works under any anchor)
rotate (boxed)1rotation — center pivot needs no compensation
rotate (group, center-feel)3rotation, x, y — Figma’s trick over legible scalars
resize (corner)2–4w, h (+ x, y from the top-left corner)
resize across zero2–3|extent|, pin, flip toggle (E-A14; out-and-back = identity)
artboard resize0end/center/span children respond in the resolved tier
move a spanned axistyped errorErr(AxisOwnedBySpan) — the wall is visible, never silent
+
+
+ + diff --git a/archive/model-v2/anchor/model-demo.html b/archive/model-v2/anchor/model-demo.html new file mode 100644 index 00000000..312fef8f --- /dev/null +++ b/archive/model-v2/anchor/model-demo.html @@ -0,0 +1,1017 @@ +anchor — how the model works + + +
+
+
+ model-v2 · anchor · model demo — 2026-07-07 +
+

One box. Content realizes into it.

+

+ The consolidated model in three interactive laws. A node is an + anchored box with typed content: the box comes from intent, content + is realized into the box at render, and nothing derivable is ever + stored. Everything below runs the spec’s own formulas (the same ones + the Rust lab asserts in 58 tests and LLMs predicted cold at 100%). +

+
+ +
+
demo a — the two-axis core
+

One box, four realizations.

+

+ Drag the box size. + The document writes are the same two fields every time (w, h) + — what differs per kind is how content re-realizes: parametric + shapes re-derive, vectors re-map from their reference rect, text re-wraps, + image paint re-fits. No points move, no matrix appears, no bake happens. +

+ +
+
+ + + + + + document writes per drag: + 2 fields +
+
+
+
shape · parametric
+
+ Descriptor = f(box). A star has no points — count 5 + + inner ratio 0.45, evaluated at the box. Stroke stays 2px. +
+ +
stored: kind=star points=5 inner=0.45
+
+
+
vector · mapped
+
+ Points live in a 24×24 reference rect (inset, frozen). + Render maps ×(box/ref). Stroke stays 2px — non-scaling + by construction. +
+ +
+ stored: ref=24×24 + 7 vertices (never rewritten) +
+
+
+
text · flowed
+
+ Content re-wraps at the imposed width (greedy, the spec metric). + Font size stays 14 — a style, not geometry. +
+ +
stored: content + size=14
+
+
+
image fill · fitted
+
+ Paint re-fits at the resolved box per fit mode — never + stretched with geometry. + +
+ +
stored: image ref + fit mode
+
+
+
+
+ +
+
demo b — the points law (e-a9)
+

Resize all you want. The blob stays the blob.

+

+ The table is the stored document data — vertices in the + reference rect. Resize writes w/h and the table never changes (Sketch + stores 0–1 points, Figma ships + normalizedSize ≠ size + nodes — same law). Only a vertex-editing gesture writes + points; then the reference bounds re-derive. +

+ +
+
+
+
+ + + + +
+ +
+
+
+
+ stored document data + FROZEN UNDER RESIZE +
+
+ w: 200   h: 200   ref: + 24×24 +
+ + + + + + +
vertexx (ref)y (ref)
+
+ resolved (read tier, materialized like Figma’s plugin API):
+ vertex #4 → +
+
+
+
+
+ +
+
demo c — the two scales
+

Resize is not scale. And scale is two different things.

+

+ One card (dotted image fill, 2px stroke, radius 10, label 12px), one + slider, three semantics. Watch the document under each: resize + writes two fields and keeps every style in px; K is a sanctioned + bake that multiplies the numbers; lens stores one op and touches no + value — the picture semantics (strokes and tiles magnify), + quarantined. +

+ +
+
+ +
+
+
+
resize
+ +
+
+
+
K · parameter scale (bake)
+ +
+
+
+
lens · picture scale (stored op)
+ +
+
+
+
+ Law 5. Styles are px-stable under resize (non-scaling-stroke by + construction). K bakes numbers through the subtree — Figma + rescale(), Sketch K, Grida parametric_scale, already shipping. A + retained picture-scale exists only as a lens op — the + SVG/Graphics-container semantics, opt-in, never a prerequisite. +
+
+
+
+ + diff --git a/archive/model-v2/anchor/pedantic-review.md b/archive/model-v2/anchor/pedantic-review.md new file mode 100644 index 00000000..cf76761a --- /dev/null +++ b/archive/model-v2/anchor/pedantic-review.md @@ -0,0 +1,209 @@ +# Pedantic review — the anchor model (MODEL.md + REPORT.md) + +2026-07-07. Four adversarial lenses (bedrock/logic, UX, engineering, +market) + an edge-case hunter, run as a panel; findings below are the +survivors after cross-checking against the docs, the lab, and — where a +claim was executable — **construction tests** (the edge-case sweep +`lab/src/bin/edge.rs` → [`edge-cases/`](./edge-cases), whose data +independently confirmed two findings before the panel returned them). + +**Verdict: the core survives — the two-axis model, layout-visible +rotation, the points-law direction, and the four-phase resolver are +genuinely evidenced. MODEL.md as a phase-3 seed needs rework in seven +places, and two market-facing seams were never measured at all.** +Findings in target order; severity per finding. + +--- + +## A. Machine-verified (a failing construction exists) + +**A1 — grow × rotation is discontinuous at θ=0 (SERIOUS, new E-A11).** +Quote: "grow expands the slot; a rotated child's box keeps its basis" +(E-A4). Constructed: fixed row, middle card `grow:1` — at θ=0 the box +fills 300 px; at θ=3° it snaps to its 60 px basis. **240 px jump in one +3° step**, measured by the edge sweep (scene `grow`). E-A4 never defined +the θ=0 boundary. _Resolution:_ inverse-envelope fill — the box takes +the largest extent whose rotated envelope fits the grown slot +(continuous at 0°, reads as "a filling card stays filling while it +turns"); or at minimum declare the pop and make the editor clear `grow` +on rotate. Conformance row owed. + +**A2 — stretch × rotation is a silent pop (SERIOUS, new E-A12).** +Constructed: `align:stretch` child stretches to 170 px tall at θ=0, +pops to 100 px at θ=3° (scene `stretch`; 70 px jump). The lab's +`contribute_aabb` branch silently skips the stretch override and the +text measure closure — an **unstated rule that is currently +load-bearing** for both "zero overlap by construction" and "single-pass +acyclic" (the bedrock lens derived the same gap from the applicability +matrix alone: §8 says `grow`/`self_align` are unconditionally "effective" +in flow, which is false for θ≠0). _Resolution:_ promote the policy to +spec text: for θ≠0 in-flow children, `self_align:Stretch` and +layout-imposed re-measure are ignored-by-rule **with report**; same +continuity treatment as A1. + +**A3 — the bool box-source cell in MODEL.md is wrong (SERIOUS).** +MODEL.md's table: `bool` = "derived (union of oriented children)". +a.md §3 and conformance **D-5** say: op-_result_ bounds (subtract-to- +empty: union is large, result is empty — they differ by construction). +Transcription drift in the consolidated statement; REPORT's lose column +concedes bool never ran, so no test caught it. _Resolution:_ fixed in +MODEL.md (this pass); lab test (subtract-to-empty) owed before phase 3. + +## B. Counterexample-verified (constructible on paper, no lab arm yet) + +**B1 — flip pivot violates D-2 for derived kinds (SERIOUS).** E-A2 says +flips are "center-applied" with no derived-kind carve-out. Counter- +example: group with `flip_x`, move child A → union widens → union +center shifts → mirrored sibling B moves in world space — the exact P6 +instability E-A1 exists to kill. Flip is unimplemented in the lab +(`grep flip lab/src` = 0 hits): the corpus forced flip's _existence_, +not its _semantics_. _Resolution:_ flip pivot per kind exactly as +rotation (center for boxed/measured, own-origin for derived); D-2-with- +flip conformance row; lab arm. + +**B2 — E-A9's write side has a stationarity counterexample +(DEALBREAKER-class for law 6 as written).** Fixed-box vector (box +48×48, ref 24×24): drag ONE vertex outward → if the ref rect re-derives +(T-5), `box/ref` changes and **every untouched vertex visibly moves**. +The disjoint-fields CRDT story also only holds for a _stored_ ref rect +— normalize-at-write renormalization rewrites every vertex on any +bound-moving edit, resurrecting the write amplification E-A9 was minted +to kill. Plus: zero-extent reference axis (any straight-line vector) → +division by zero at render, reachable from a shipping document (N-2 +promises NaN-free). _Resolution set (lock before phase 3):_ ref rect = +**free intent** (explicitly exempt from law 1 — same status as Figma's +`normalizedSize`, which is bake-time state, not point bounds); **forbid +origin/bounds renormalization as a gesture side effect**; vertex-edit = +declared atomic write-set with a stationarity guarantee; zero-extent +axis → translation-only mapping (POL; check a straight-line .fig +fixture first — io-figma harness exists); rewrite T-5 with Auto/Fixed +arms; implement the vector kind in the lab with a resize∥vertex-edit +test. + +**B3 — law 8 contradicts itself and the lab (MODERATE).** "The op layer +never hard-walls… a rejected op leaves the document byte-identical" — +both halves can't rule. The lab (cited as proof) _rejects_: +`Err(AxisOwnedBySpan)`, `Err(OwnedByLayout)`; M-2 locks a typed-error +list. _Resolution:_ two enumerated regimes — the re-target set (writes +that coerce, with report) and the reject set (M-2's list, byte- +identical) — and MODEL.md says which is which. + +**B4 — reads-materialize/writes-retarget has a counterexample (SERIOUS, +editor seam).** Type W into the inspector of a `grow:1` (or stretched) +child: the echo cannot equal the typed value unless the write also +clears `grow`/`self_align`. Figma's answer (clears fill on W write) is +the researched precedent. _Resolution:_ editor.md rule — a W/H write on +a grown/stretched child clears the growth intent (session-memory +restorable); law 7 carve-out stated. + +**B5 — group rotation typed into an inspector lurches (SERIOUS, editor +seam).** Raw `rotation` write on a group = origin pivot (E-A1's +"bindings place the origin" can sit far from visible content); the +center-feel result requires the 3-write OP-ROT-3 gesture. A numeric +inspector field is a raw write today. _Resolution:_ inspector rotation +on derived kinds routes through OP-ROT-3 (compensated); spec'd in +editor.md, not left to implementers. (The edge demo's `group` scene +shows flow placement stays centered — the resolver's slot-centering +hides the pivot in flow; free context is where the lurch lives.) + +## C. Leaked uncertainty / overclaims in MODEL.md prose + +**C1 — "E-A1…E-A10 folded into one coherent statement" is false +(MODERATE, verified by grep).** E-A4, E-A5, E-A7, E-A8 are absent. +Worse: E-A5 (underdetermined bindings error-by-rule) is the _acyclicity +guard_ — without it stated, "single-pass, acyclic" has a constructible +cycle (Span ← parent hug ← child extents). _Resolution:_ fold or +narrow the claim (this pass narrows it); E-A5 stated next to the +acyclicity claim. + +**C2 — locality overclaim (MODERATE).** "18 µs locality" drops E4's own +qualifier ("under clean parent boxes"); a text edit in a hug chain +propagates to the nearest fixed-extent ancestor (the flagship card-list +example violates the clean-parent precondition). And "subtree-local +invalidation by structure" presents as achieved what REPORT quarantines +("no incremental resolver"). _Resolution:_ state the dirty-propagation +rule (up the measure chain to the nearest fixed-extent ancestor) and +the 18 µs precondition. + +**C3 — E3 citation drops its own quarantine (MINOR but strategic).** +"LLMs predicted cold at 100%" without "n=3 models, 6 documents, +authoring-only, no stable ids". Law 7's write half (agent edit-in-place) +is _open_, not proven — no ids means positional identity breaks on +reorder+edit. _Resolution:_ carry the protocol caveat; split law 7 +(reads proven / IR writes pending stable ids). + +**C4 — M-4 is an INV the chosen encoding verifiably fails (MODERATE).** +E-A6 measured FlatBuffers dropping unknown fields on old-schema +re-encode, yet conformance M-4 still says INV "preserves unknown +content through RMW". _Resolution:_ decide E-A6 (buffer-retention vs +version-gated writers) or downgrade M-4 to POL with the declared +answer — before phase 3, since S-5/forward-compat cite it. + +**C5 — lens `Scale` is a scalar; Framer-Graphic semantics are +box-proportional (MODERATE, honest-trade finding).** E7 oversold the +equivalence: a retained scalar cannot track the box, so "icon scales +proportionally _when its box resizes_" is still unrepresentable +retained. _Resolution:_ declare the trade explicitly (substitutes: K +bake, per-leaf mapped vectors) or add a box-relative scale variant to +the lens vocabulary later — additive either way. + +**C6 — the lens has no opt-in (MODERATE, UX).** MODEL.md calls the +proportional world "the opt-in lens"; editor.md has no lens-creating +or lens-resizing operation — the only producer is SVG import. Resize +handles on a lens are undefined (pre-ops box vs post-ops visual). +_Resolution:_ OP rows: "wrap in lens", "lens resize re-targets the +Scale op", hit/chrome tracks post-ops visual. + +## D. The two unmeasured market seams + +**D1 — "lossless transpile to CSS" is unfalsifiable as claimed +(DEALBREAKER-class for the web story).** finale.md's concession table +carries it; layout-visible rotation **has no CSS flexbox equivalent** +— a `grow:1 rotation:20` child cannot be projected losslessly. Grida +ships a DOM-rendered tenant-site surface today. _Resolution:_ **E8** — +transpile the E1 document to HTML/CSS, measure the loss class, decide +the projection mechanism (per-breakpoint bake of resolved geometry / +JS resolver runtime / declared degradation table). Until E8 runs, the +web bill is unpaid, not cheap. + +**D2 — the Figma corpus was never scanned (SERIOUS).** Triage #3 made +Figma-convertibility the hard requirement; E5 measured a _million SVG +transforms_ and zero `.fig` nodes. Known concrete gap: Figma's SCALE +constraint has no v1 equivalent (percent pins are deferred). _Resolution:_ +**E9** — E5-style scanner over the io-figma corpus counting constraint +types (esp. SCALE), rotated children in auto-layout, +`normalizedSize≠size` vectors, boolean nesting, negative determinants. +Also: declare the export posture (flatten-to-frames lossy table, or +export-is-a-non-goal) — round-trip silence reads as a hidden "no". + +**D3 — the Figma community record predicts the #1 support ticket +(SERIOUS, UX-market).** Figma's forum carries recurring tickets against +exactly this behavior class ("prevent rotated images increasing the +component's bounding box"). E1 measured smoothness, not user reaction. +_Resolution:_ the escapes are launch-blocking editor affordances, not +notes — rotate-HUD one-click "keep slot fixed" (wrap/lens re-target) + +envelope readout (E-A7). Also fold the "Fill" sugar (`width="fill"` in +IR/inspector redirecting to grow/stretch/span with report) — one user +concept, three intents, incumbents spell it as one word. + +## Cleared by the panel (pressed, survived) + +Layout-visible rotation itself (Figma parity X-FIG-1 + E1 measurement); +the no-Fill _model_ (given D3's sugar); reads-materialize for +copy/paste (natural); origin-placement E-A1 (D-2 holds, lab-tested); +the two-scales split (K vs lens, given C5/C6 fixes); text-measure- +unrotated (industry-standard dodge, edge scene reads naturally); wrap +line-hop (discrete by nature, CSS-equivalent; editor should animate); +rotated-frame rigidity, space-between symmetry, two-envelope +composition — all smooth in the sweep with sub-bound step sizes. + +## Disposition + +MODEL.md surgically corrected this pass: A3 (bool cell), C1 (folded- +claim narrowed + E-A5 stated), C2 (locality qualified), B3 (law 8 two +regimes), C3 (E3 caveat), law 6 marked open-pending-B2. Everything +else lands as phase-3 lock conditions: **E-A11** (A1), **E-A12** (A2), +the B2 lock set, B1 flip pivots, B4/B5/C6 editor.md rows, C4 M-4 +decision, C5 declared trade, **E8** and **E9** as new ledger +experiments. The demo for A1/A2 and the cleared scenes is +[`edge-cases/`](./edge-cases) (interactive; discontinuity meter). diff --git a/archive/model-v2/anchor/pluggable-layout/SKETCH.md b/archive/model-v2/anchor/pluggable-layout/SKETCH.md new file mode 100644 index 00000000..74827400 --- /dev/null +++ b/archive/model-v2/anchor/pluggable-layout/SKETCH.md @@ -0,0 +1,72 @@ +# E6 — pluggable layout, the curiosity sketch + +Status: **exploratory, non-binding** (triage #28: "never core — but I +would LOVE to see what lands if I picked 'core must anticipate'"). +Nothing here is a commitment; this is the owed look down the road not +taken. + +## The finding: the seam already exists + +Building the E1/E4 resolver revealed that the anchor model already +contains a layout-plugin interface _by accident of its own discipline_. +Phase L's contract per container is exactly: + +``` +layout: (container content box, [child contributions]) → [slots] +child placement: box center := slot center +``` + +where a **contribution** is `(main, cross)` — already an abstraction, because +rotated children submit their AABB, not their box (E1). Flex never knows +whether it is placing a box, a rotated envelope, or a group union. That +is the whole plugin surface: + +``` +trait LayoutMode { + fn measure(&self, children: &[Contribution], avail: Avail) -> Size; // hug + fn place(&self, children: &[Contribution], content: Size) -> Vec; +} +``` + +`None` (bindings) and `Flex` (Taffy) are the two built-ins. Grid is a +third implementation of the same trait — which is why a.md could call +grid "an additive future mode variant" without hand-waving. + +## What the exotic modes would look like + +- **Radial**: slots on a circle; parameters `radius`, `start_angle`, + `spread`. Child rotation-to-tangent is _not_ the layout's job — it + writes nothing; a `follow: tangent` child flag would resolve rotation + from the slot angle (a resolved-tier value, never serialized — the + "derivable ⇒ not encodable" law extends cleanly). +- **Masonry**: `place` with per-column running heights — pure function of + contributions, no new model concepts at all. +- **Text-on-path**: the one that _breaks_ the seam. Slots stop being + rects (they become arc-length frames with tangents), and measurement + couples to glyph shaping. This wants to live inside the `text` payload + (a content concern), not in the container protocol. The seam's limit + is: **slots are axis-aligned rects**; anything needing oriented slots + is payload territory. + +## Why it stays out of core (unchanged, now with reasons priced) + +1. **E3 is the veto.** The model's proven superpower is that an LLM + predicts geometry from a 150-line grammar. Every pluggable mode + multiplies the grammar; a _user-definable_ mode (wasm hook?) destroys + cold prediction entirely — resolution would no longer be closed-form + from the document alone. That trade is not worth radial menus. +2. **Conformance surface.** Each mode adds a suite the size of L-\*. + Flex is testable against Chromium; radial/masonry have no oracle but + us (§9-style authored spec each time). +3. **Nothing is precluded.** `LayoutBehavior.mode` is an fbs enum/union — + additive by the E2-verified evolution rules. The trait boundary above + is an _internal_ engine shape; adopting it costs a refactor, not a + format migration. Anticipation is therefore free — which is the best + argument for not spending on it now. + +## One sentence of verdict + +The core should _stay_ two modes; but phase 4 should implement Phase L +against the `(contributions → slots)` trait anyway, because E1's rotated +AABBs already forced that abstraction into existence — the plugin door +comes free with the rotation feature, locked from the format side. diff --git a/archive/model-v2/anchor/proof.html b/archive/model-v2/anchor/proof.html new file mode 100644 index 00000000..bad085bb --- /dev/null +++ b/archive/model-v2/anchor/proof.html @@ -0,0 +1,3876 @@ +anchor — visual proof + + +
+
+
+ model-v2 · anchor · proof run — 2026-07-07 +
+

The list makes room.

+

+ Every number and pixel on this page comes from the lab in + model-v2/anchor/ — the resolver that implements the anchor spec, + the sweeps it ran, and the agents that were tested against it. Three kinds + of proof, in order of strength: +

+ +
+
+
runtime
+
it runs, live
+

+ The E1 demo below is the resolver's actual output, scrubbable. On your + machine: + cargo test → + 56/56. +

+
+
+
image
+
it renders, exactly
+

+ SVG frames emitted by the resolver at key angles, magenta dashes = + computed world AABBs. +

+
+
+
record
+
it is written down
+

+ metrics.csv, truth.txt, prediction files, byte-fixpoint binaries + — every claim re-derivable. +

+
+
+ +
+ LAB 56/56 tests + E1 layout-visible locked + E2 byte-fixpoint + E3 LLM 22/22 ×2 + E4 10k → 5.4 ms + E5 1M transforms +
+
+ +
+
exhibit e1 — the deciding experiment, live
+

Rotate the middle card. Watch both worlds.

+

+ Same document, same resolver, one flag. Top: + anchor — the rotated card + participates by its oriented envelope, siblings make room, the hug + container breathes. Bottom: + CSS control — layout is frozen, + rotation is paint-only, and the red wash is sibling overlap that no + document field expresses. +

+ +
+
+
+
anchor — aabb participates
+ +
+
+
css control — visual-only
+ +
+
+
+ θ = 0° + + + + + +
+
+ anchor container 220.0 px + anchor overlap 0 px² + control overlap 0 px² + rotate gesture 1 write / step +
+
+ +
+
+
Container width vs θ
+
+ anchor breathes 220→276.6 px, peak at + θ*=atan(h/w)≈59°; control flat +
+ +
+
+
Sibling overlap area vs θ
+
+ anchor: zero at every angle · control: up to 1,830 px² +
+ +
+
+
Third card position vs θ
+
+ smooth — max step 3.45 px per 2°, inside the + √(w²+h²)·Δθ = 4.07 px bound +
+ +
+
+
Verdict
+
+ conformance R-3 and editor OP-ROT-2: POL → INV +
+

+ Locked layout-visible. Zero overlap by construction and by + measurement; displacement continuous through + 0°/90°/180°; rotation stays a one-field write. + Breathing during a full spin (56.6 px) is a hug-container phenomenon + and is exactly why motion rotation targets a lens channel — the + two-lane rule, now measured rather than argued. +

+
+
+ +
+
+ + + + anchor: AABB participates — θ=0° + + + + + + + + + + + + + + control: visual-only (CSS) — θ=0° + + + + + + + + + + + + +
θ = 0° — resolver SVG output
+
+
+ + + + anchor: AABB participates — θ=45° + + + + + + + + + + + + + + control: visual-only (CSS) — θ=45° + + + + + + + + + + + + +
+ θ = 45° — envelope 113.1×113.1 +
+
+
+ + + + anchor: AABB participates — θ=90° + + + + + + + + + + + + + + control: visual-only (CSS) — θ=90° + + + + + + + + + + + + +
+ θ = 90° — slot is exactly 100×60 +
+
+
+
+ +
+
exhibit e3 — can a model predict it cold?
+

Two fresh agents, a 150-line grammar, 22/22 exact.

+

+ Three agents were handed only the grammar and six documents — no + tools, no resolver — and asked for every resolved box. The hardest + probe is the E1 rule itself (p5, below). Frontier agents matched the + resolver on all 22 values within 0.5 px. +

+ +
+
<!-- probe p5 — rotated card in a hug row -->
+<frame name="list" w="auto" h="140"
+       layout="flex" gap="10" padding="10"
+       cross="center">
+  <shape name="a" kind="rect" w="60" h="100"/>
+  <shape name="b" kind="rect" w="60" h="100"
+         rotation="90"/>
+  <shape name="c" kind="rect" w="60" h="100"/>
+</frame>
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
p5 valueresolverfable-afable-bhaiku-c
list (hug w)260260260260
b box x,y100, 20100, 20100, 2050, 40
b world AABB80, 40, 100×60exactexactx=30
c (pushed) x190190190190
all 6 probes22 values22/2222/2217/22
+
+
+

+ Even the small-model control applied the rotation-in-flow + rule correctly — its misses were an off-by-one character + count and placing a box at slot-start instead of slot-center. Slips, not + structural confusion. Records: + text-ir/truth.txt · predictions/* +

+
+ +
+
exhibit e2 — the file format
+

Encode → decode → encode: byte-identical.

+

+ The quartet document through flatc 25.12: JSON → binary → JSON + → binary produces the same bytes. And "unset" is structural — + read the decoded output: +

+
+
// grow-text node — decoded JSON
+{
+  "id": 5, // no x, no y keys at all
+  "grow": 1.0  // ⇒ Pin{Start,0} by rule
+}
+// flex frame — height IS auto, structurally
+{
+  "width_type": "SizeFixed",
+  "width": { "value": 400.0 },
+  "height_type": "SizeAuto",
+  "height": { }
+}
+
+
+$ ./run.sh
+S-1 FIXPOINT: byte-identical
+M-4 forward:  v1 binary decodes under v2 schema
+M-4 backward: v2 binary decodes under v1 schema
+ max_lines: 0 ≡ unset — now unrepresentable +

+ One policy owed for phase 3: re-encoding through an old schema + drops unknown fields — read-skip is free, read-modify-write + preservation is not. +

+
+
+
+ +
+
exhibit e4 — the resolver, timed
+

10,000 nodes in 5.4 ms, unoptimized.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
scenenodesfull resolvethroughput
flat canvas (⅐ rotated)10,0015.42 ms1,844 nodes/ms
flex cards — nested hug + text measure (worst case)5,78524.4 ms237 nodes/ms
mixed groups + rotated-in-flow10,0038.21 ms1,218 nodes/ms
one card subtree (locality bound per leaf edit)618 µs
+
+

+ Single-threaded, zero caching, and the lab double-runs layout on hug + containers — this is the floor. Scaling is linear (10× nodes + → 9.6× time in the all-flex worst case). What it replaces: the + 26-arm branch forest, the atan2 transform reconstruction, and + MIN_SIZE_DIRTY_HACK. +

+
+ +
+
exhibit e5 — a million transforms
+

The corpus re-ranked the escape hatch.

+

+ 7,138 SVGs, 1,003,787 transforms scanned. The lens was designed for skew + — but genuine shear is under 1%. The real finding is + flip at 26%: single-axis mirrors must be native header bits, or the + "quarantine" fires on a quarter of wild files. +

+ +
+ translate + 9.1% + rotate 1.3% + scale= 19.8% + scale≠ + 42.8% + flip 26.1% → + goes native + shear 0.95% → + lens +
+
+ +
+
reproduce it
+

Runtime proof, on your machine.

+
+
from the repo root
+
open model-v2/anchor/rotation-in-flow/demo.html   # the offline twin of the demo above
+cd model-v2/anchor/lab
+cargo test                        # 56/56 conformance-derived tests
+cargo run --bin e1                # regenerate the sweep, frames, metrics
+cargo run --bin e3 -- truth       # ground truth an agent must match
+cargo run --release --bin e4      # the timings above
+
+

+ Full narrative, amendments E-A1…E-A7, and the honest lose column: + model-v2/anchor/REPORT.md. + Nothing is committed to git — the workbench is yours to review. +

+
+
+ + diff --git a/archive/model-v2/anchor/resolver-spike/README.md b/archive/model-v2/anchor/resolver-spike/README.md new file mode 100644 index 00000000..18e6b57f --- /dev/null +++ b/archive/model-v2/anchor/resolver-spike/README.md @@ -0,0 +1,40 @@ +# E4 — resolver spike + +**Question.** Does the four-phase resolution (measure → layout → +transforms → bounds, models/anchor.md §6) hold up as an engine architecture — +throughput, scaling, invalidation shape — against the current +`crates/grida` pipeline (`cache/geometry.rs` + `layout/engine.rs`)? + +**Method.** The lab resolver ([`../lab/src/resolve.rs`](../lab/src/resolve.rs)) +is a faithful §6 implementation over Taffy 0.9 (the engine's own layout +crate, same version). [`../lab/src/bin/e4.rs`](../lab/src/bin/e4.rs) +benches full resolution on three scene shapes at 1k/10k nodes +(median of 11, release, single-threaded, M-series laptop). + +## Measured (2026-07-07) + +| scene | nodes | full resolve | throughput | +| ----------------------------------------------------- | ------ | ------------ | -------------- | +| flat canvas (free shapes, ⅐ rotated) | 1,001 | 1.05 ms | 952 nodes/ms | +| flat canvas | 10,001 | 5.42 ms | 1,844 nodes/ms | +| flex cards (nested hug column-in-row, text measure) | 577 | 2.55 ms | 226 nodes/ms | +| flex cards | 5,785 | 24.4 ms | 237 nodes/ms | +| mixed groups + flex (rotated-in-flow on the hot path) | 10,003 | 8.21 ms | 1,218 nodes/ms | +| single card subtree (locality bound) | 6 | **18 µs** | — | + +- **N-4 linearity holds**: 10× nodes → 5.2×–9.6× time across shapes + (sub-linear on flat scenes from cache warmth; the all-flex worst case + is cleanly linear). +- **Locality**: a leaf edit under clean parent boxes re-resolves one + container subtree — measured 18 µs per card. The one-way phase order + (measure never reads position; contributions never read assigned + position) is what makes "clean parent ⇒ subtree-local" a _structural_ + guarantee rather than a cache heuristic. +- Flex-heavy costs ~5–8× per node vs free placement. Two known lab + inefficiencies inflate it: hug containers run Taffy twice (intrinsic + + definite pass), and each container builds a fresh `TaffyTree`. The + production engine's single-tree build (`layout/engine.rs`) removes + both; treat 237 nodes/ms as the floor, not the ceiling. + +The architecture comparison and dependency findings are in +[`verdict.md`](./verdict.md). diff --git a/archive/model-v2/anchor/resolver-spike/verdict.md b/archive/model-v2/anchor/resolver-spike/verdict.md new file mode 100644 index 00000000..c20ce46a --- /dev/null +++ b/archive/model-v2/anchor/resolver-spike/verdict.md @@ -0,0 +1,61 @@ +# E4 verdict — resolver architecture + +**Decision: the four-phase resolver is viable as the phase-4 +architecture — it is simpler than what it replaces, its costs are +in-budget unoptimized, and its structure gives locality for free.** +Plus two dependency findings that phase 4 must carry. + +## vs `cache/geometry.rs` (the branch forest) + +What the lab resolver structurally _does not contain_, because the model +made the cases unrepresentable: + +| current pipeline (`crates/grida`) | lab resolver | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `resolve_layout` (geometry.rs:754): per-kind branch forest — 26 `match`/`if let` arms reconciling three geometry models | one uniform header path + a 3-way box-source split (declared / measured / derived) | +| `AffineTransform::new(l.x, l.y, geo.schema_transform.rotation())` (geometry.rs:823 …): rebuilds leaf transforms through `atan2`, destroying scale/skew (X-SELF-2) | no matrix in the document to destroy; `from_box_center` is _the_ constructor (Phase T) — no `atan2` anywhere in steady state | +| `NodeGeoData` carries `schema_transform` **and** a separate rotation — two truths to keep coherent | the resolved tier holds resolved values only; intent lives in the document | +| `MIN_SIZE_DIRTY_HACK = 1.0` px floors (geometry.rs:812–869) patching text/markdown zero-size cases | G-E5's declared rule; zero-size resolves as zero, reported — no floors (the hack is a sentinel workaround the model no longer needs) | +| ICB special regime (root children's transforms ignored) | root is an ordinary viewport-bound frame (X-SELF-5 break, tested) | + +That 26-arm forest is not "bad code" — it is the _interest payment_ on +three coexisting geometry models. The spike's point is that the payment +stops when the model unifies: the whole lab resolver is ~700 lines +including the E1 fork, reports, and both derived-box semantics. + +## Performance posture + +10k free nodes in 5.4 ms and the all-flex worst case at 24 ms — +single-threaded, zero caching, double-running Taffy on hug containers — +comfortably clears interactive budgets before any of the production +engine's machinery (single Taffy tree, damage tracking, parallelism) is +applied. Phase 4 carries over `layout/engine.rs`'s single-tree build and +adds the anchor semantics on top; nothing measured here suggests the +model itself adds cost — the AABB contribution for rotated-in-flow +children is two `abs`-weighted products per child (`mixed` scene: 1,218 +nodes/ms _with_ rotated flow children on the hot path). + +## Dependency findings (Taffy 0.9.2) — carried to phase 4 + +1. **Rounding default**: Taffy pixel-snaps unless `disable_rounding()` is + called. L-7 declares resolution unquantized (snapping is paint's job) + — the engine must disable rounding or L-7 drifts silently. +2. **Intrinsic-pass grow inflation**: in the max-content sizing pass a + _growable_ item's contribution is floored by the container's own + padding (`.max(main_content_box_inset)`, flexbox.rs — the source + comments admit trial-and-error provenance). Symptom: hug column with + `padding:16` + a growable one-line text hugs to 112 instead of 99.2 — + deviating from both L-3 ("grow distributes only definite free space") + and Chromium. Lab enforcement: strip grow factors in indefinite-main + intrinsic runs; the definite re-run applies them. Phase 4 needs the + same guard (or an upstream Taffy fix) — and L-3 needs a conformance + test pinned against Chromium, not against Taffy. + +## Spec consequences + +- N-4 (linear scaling, bounded invalidation) — evidenced; the phase-order + acyclicity note in §6 should be promoted from an implementation remark + to a normative invariant, since locality _derives_ from it. +- The two-pass hug (intrinsic then definite) is inherent to + hug-with-layout in any engine; single-tree Taffy hides it inside one + `compute_layout`. No model change needed. diff --git a/archive/model-v2/anchor/rotation-in-flow/README.md b/archive/model-v2/anchor/rotation-in-flow/README.md new file mode 100644 index 00000000..4d69bbc9 --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/README.md @@ -0,0 +1,39 @@ +# E1 — rotation-in-flow prototype + +**Question.** A rotated child inside a flex flow: does layout see it — the +oriented AABB participates and siblings make room (`anchor` §5) — or is +rotation paint-only, layout frozen, overlap correct (CSS `transform` +semantics)? The model-v2 triage left this as the spec's only open core +semantic (conformance **R-3**, editor **OP-ROT-2**), tilted layout-visible, +to be decided by measurement, not argument (triage #5). + +**Method.** Both semantics implemented behind one flag +([`RotationInFlow`](../lab/src/resolve.rs)) in the same resolver — nothing +else differs (guarded by the `flag_scope_is_limited_to_flow_rotation` +test). The triage-#27 scene — three 60×100 cards in a hug row (gap 10, +pad 10), middle card rotated — swept θ = 0…360° in 2° steps under both +modes, plus a fixed-width-container variant. Every step applied through +the op layer, asserting rotation stays a **1-field write** in flow. + +**Artifacts.** + +- [`metrics.csv`](./metrics.csv) — per-frame container width, sibling + position, peak sibling AABB overlap, per-step displacement. +- [`frames/theta_*.svg`](./frames) — side-by-side snapshots (anchor arm + above, visual-only control below, magenta dashed = world AABBs). +- [`demo.html`](./demo.html) — interactive scrubber + play button over the + precomputed frames; open in any browser to _feel_ both arms. +- Driver: [`../lab/src/bin/e1.rs`](../lab/src/bin/e1.rs) + (`cargo run --bin e1` from `../lab`). + +**Measured** (sweep summary, reproduced by the driver): + +| metric | anchor (AABB participates) | visual-only control | +| --------------------------- | -------------------------------------------------------- | ----------------------------------- | +| sibling overlap, any θ | **0 px², always** | up to **1830 px²** (≈31% of a card) | +| container (hug) | breathes 220 → 276.6 px (peak at θ\*≈59°) | frozen at 220 px | +| sibling displacement per 2° | ≤ 3.45 px, smooth (analytic bound √(w²+h²)·Δθ = 4.07 px) | 0 | +| envelope peak | θ\* = atan(h/w) ≈ 59°, **not** 90° | n/a | +| rotate gesture | 1 field write | 1 field write | + +The verdict lives in [`verdict.md`](./verdict.md). diff --git a/archive/model-v2/anchor/rotation-in-flow/demo.html b/archive/model-v2/anchor/rotation-in-flow/demo.html new file mode 100644 index 00000000..0679f572 --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/demo.html @@ -0,0 +1,2257 @@ + + +E1 — rotation-in-flow: anchor vs visual-only + +

E1 — rotate a card inside an auto-flowing list

+

+ Top: anchor (rotated AABB participates — siblings make room, container + breathes).
+ Bottom: visual-only control (CSS transform semantics — layout frozen, + overlap). +

+
+ + + +
+ + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_000.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_000.svg new file mode 100644 index 00000000..c4d19f79 --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_000.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=0° + + + + + + + + + + + +control: visual-only (CSS) — θ=0° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_015.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_015.svg new file mode 100644 index 00000000..df40d010 --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_015.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=15° + + + + + + + + + + + +control: visual-only (CSS) — θ=15° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_030.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_030.svg new file mode 100644 index 00000000..19d544ae --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_030.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=30° + + + + + + + + + + + +control: visual-only (CSS) — θ=30° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_045.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_045.svg new file mode 100644 index 00000000..1bef3699 --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_045.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=45° + + + + + + + + + + + +control: visual-only (CSS) — θ=45° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_060.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_060.svg new file mode 100644 index 00000000..8c29c75c --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_060.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=60° + + + + + + + + + + + +control: visual-only (CSS) — θ=60° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_075.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_075.svg new file mode 100644 index 00000000..da75c455 --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_075.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=75° + + + + + + + + + + + +control: visual-only (CSS) — θ=75° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/frames/theta_090.svg b/archive/model-v2/anchor/rotation-in-flow/frames/theta_090.svg new file mode 100644 index 00000000..672f255a --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/frames/theta_090.svg @@ -0,0 +1,26 @@ + + +anchor: AABB participates — θ=90° + + + + + + + + + + + +control: visual-only (CSS) — θ=90° + + + + + + + + + + + diff --git a/archive/model-v2/anchor/rotation-in-flow/metrics.csv b/archive/model-v2/anchor/rotation-in-flow/metrics.csv new file mode 100644 index 00000000..61303d7b --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/metrics.csv @@ -0,0 +1,182 @@ +theta,aabb_container_w,aabb_card2_x,aabb_overlap,visual_container_w,visual_card2_x,visual_overlap,fixedw_aabb_card2_x,fixedw_aabb_overlap +0,220.000,150.000,0.000,220.000,150.000,0.000,150.000,0.000 +2,223.453,153.453,0.000,220.000,150.000,0.000,153.453,0.000 +4,226.829,156.829,0.000,220.000,150.000,0.000,156.829,0.000 +6,230.124,160.124,0.000,220.000,150.000,0.000,160.124,0.000 +8,233.333,163.333,0.000,220.000,150.000,0.000,163.333,0.000 +10,236.453,166.453,0.000,220.000,150.000,0.000,166.453,0.000 +12,239.480,169.480,0.000,220.000,150.000,0.000,169.480,0.000 +14,242.410,172.410,0.000,220.000,150.000,120.497,172.410,0.000 +16,245.239,175.239,0.000,220.000,150.000,261.972,175.239,0.000 +18,247.965,177.965,0.000,220.000,150.000,398.255,177.965,0.000 +20,250.584,180.584,0.000,220.000,150.000,529.179,180.584,0.000 +22,253.092,183.092,0.000,220.000,150.000,654.585,183.092,0.000 +24,255.486,185.486,0.000,220.000,150.000,774.319,185.486,0.000 +26,257.765,187.765,0.000,220.000,150.000,888.239,187.765,0.000 +28,259.924,189.924,0.000,220.000,150.000,996.201,189.924,0.000 +30,261.962,191.962,0.000,220.000,150.000,1098.077,191.962,0.000 +32,263.875,193.875,0.000,220.000,150.000,1193.741,193.875,0.000 +34,265.662,195.662,0.000,220.000,150.000,1283.078,195.662,0.000 +36,267.320,197.320,0.000,220.000,150.000,1365.978,197.320,0.000 +38,268.847,198.847,0.000,220.000,150.000,1442.340,198.847,0.000 +40,270.241,200.241,0.000,220.000,150.000,1512.071,200.241,0.000 +42,271.502,201.502,0.000,220.000,150.000,1575.088,201.502,0.000 +44,272.626,202.626,0.000,220.000,150.000,1631.312,202.626,0.000 +46,273.613,203.613,0.000,220.000,150.000,1680.675,203.613,0.000 +48,274.462,204.462,0.000,220.000,150.000,1723.117,204.462,0.000 +50,275.172,205.172,0.000,220.000,150.000,1758.586,205.172,0.000 +52,275.741,205.741,0.000,220.000,150.000,1787.039,205.741,0.000 +54,276.169,206.169,0.000,220.000,150.000,1808.441,206.169,0.000 +56,276.455,206.455,0.000,220.000,150.000,1822.767,206.455,0.000 +58,276.600,206.600,0.000,220.000,150.000,1829.999,206.600,0.000 +60,276.603,206.603,0.000,220.000,150.000,1830.128,206.603,0.000 +62,276.463,206.463,0.000,220.000,150.000,1821.768,206.463,0.000 +64,276.182,206.182,0.000,220.000,150.000,1768.648,206.182,0.000 +66,275.759,205.759,0.000,220.000,150.000,1707.237,205.759,0.000 +68,275.195,205.195,0.000,220.000,150.000,1638.171,205.195,0.000 +70,274.490,204.490,0.000,220.000,150.000,1562.135,204.490,0.000 +72,273.647,203.647,0.000,220.000,150.000,1479.866,203.647,0.000 +74,272.664,202.664,0.000,220.000,150.000,1392.148,202.664,0.000 +76,271.545,201.545,0.000,220.000,150.000,1299.806,201.545,0.000 +78,270.289,200.289,0.000,220.000,150.000,1203.704,200.289,0.000 +80,268.900,198.900,0.000,220.000,150.000,1104.738,198.900,0.000 +82,267.377,197.377,0.000,220.000,150.000,1003.832,197.377,0.000 +84,265.724,195.724,0.000,220.000,150.000,901.934,195.724,0.000 +86,263.942,193.942,0.000,220.000,150.000,800.009,193.942,0.000 +88,262.033,192.033,0.000,220.000,150.000,699.036,192.033,0.000 +90,260.000,190.000,0.000,220.000,150.000,600.000,190.000,0.000 +92,262.033,192.033,0.000,220.000,150.000,699.036,192.033,0.000 +94,263.942,193.942,0.000,220.000,150.000,800.009,193.942,0.000 +96,265.724,195.724,0.000,220.000,150.000,901.934,195.724,0.000 +98,267.377,197.377,0.000,220.000,150.000,1003.831,197.377,0.000 +100,268.900,198.900,0.000,220.000,150.000,1104.737,198.900,0.000 +102,270.289,200.289,0.000,220.000,150.000,1203.703,200.289,0.000 +104,271.545,201.545,0.000,220.000,150.000,1299.806,201.545,0.000 +106,272.664,202.664,0.000,220.000,150.000,1392.149,202.664,0.000 +108,273.647,203.647,0.000,220.000,150.000,1479.866,203.647,0.000 +110,274.490,204.490,0.000,220.000,150.000,1562.135,204.490,0.000 +112,275.195,205.195,0.000,220.000,150.000,1638.172,205.195,0.000 +114,275.759,205.759,0.000,220.000,150.000,1707.237,205.759,0.000 +116,276.182,206.182,0.000,220.000,150.000,1768.646,206.182,0.000 +118,276.463,206.463,0.000,220.000,150.000,1821.768,206.463,0.000 +120,276.603,206.603,0.000,220.000,150.000,1830.127,206.603,0.000 +122,276.600,206.600,0.000,220.000,150.000,1829.999,206.600,0.000 +124,276.455,206.455,0.000,220.000,150.000,1822.768,206.455,0.000 +126,276.169,206.169,0.000,220.000,150.000,1808.441,206.169,0.000 +128,275.741,205.741,0.000,220.000,150.000,1787.039,205.741,0.000 +130,275.172,205.172,0.000,220.000,150.000,1758.586,205.172,0.000 +132,274.462,204.462,0.000,220.000,150.000,1723.117,204.462,0.000 +134,273.613,203.613,0.000,220.000,150.000,1680.675,203.613,0.000 +136,272.626,202.626,0.000,220.000,150.000,1631.312,202.626,0.000 +138,271.502,201.502,0.000,220.000,150.000,1575.089,201.502,0.000 +140,270.241,200.241,0.000,220.000,150.000,1512.071,200.241,0.000 +142,268.847,198.847,0.000,220.000,150.000,1442.340,198.847,0.000 +144,267.320,197.320,0.000,220.000,150.000,1365.978,197.320,0.000 +146,265.662,195.662,0.000,220.000,150.000,1283.078,195.662,0.000 +148,263.875,193.875,0.000,220.000,150.000,1193.741,193.875,0.000 +150,261.962,191.962,0.000,220.000,150.000,1098.077,191.962,0.000 +152,259.924,189.924,0.000,220.000,150.000,996.201,189.924,0.000 +154,257.765,187.765,0.000,220.000,150.000,888.239,187.765,0.000 +156,255.486,185.486,0.000,220.000,150.000,774.319,185.486,0.000 +158,253.092,183.092,0.000,220.000,150.000,654.585,183.092,0.000 +160,250.584,180.584,0.000,220.000,150.000,529.179,180.584,0.000 +162,247.965,177.965,0.000,220.000,150.000,398.254,177.965,0.000 +164,245.239,175.239,0.000,220.000,150.000,261.972,175.239,0.000 +166,242.410,172.410,0.000,220.000,150.000,120.497,172.410,0.000 +168,239.480,169.480,0.000,220.000,150.000,0.000,169.480,0.000 +170,236.453,166.453,0.000,220.000,150.000,0.000,166.453,0.000 +172,233.333,163.333,0.000,220.000,150.000,0.000,163.333,0.000 +174,230.124,160.124,0.000,220.000,150.000,0.000,160.124,0.000 +176,226.829,156.829,0.000,220.000,150.000,0.000,156.829,0.000 +178,223.453,153.453,0.000,220.000,150.000,0.000,153.453,0.000 +180,220.000,150.000,0.000,220.000,150.000,0.000,150.000,0.000 +182,223.453,153.453,0.000,220.000,150.000,0.000,153.453,0.000 +184,226.829,156.829,0.000,220.000,150.000,0.000,156.829,0.000 +186,230.124,160.124,0.000,220.000,150.000,0.000,160.124,0.000 +188,233.333,163.333,0.000,220.000,150.000,0.000,163.333,0.000 +190,236.453,166.453,0.000,220.000,150.000,0.000,166.453,0.000 +192,239.480,169.480,0.000,220.000,150.000,0.000,169.480,0.000 +194,242.410,172.410,0.000,220.000,150.000,120.497,172.410,0.000 +196,245.239,175.239,0.000,220.000,150.000,261.972,175.239,0.000 +198,247.965,177.965,0.000,220.000,150.000,398.255,177.965,0.000 +200,250.584,180.584,0.000,220.000,150.000,529.179,180.584,0.000 +202,253.092,183.092,0.000,220.000,150.000,654.585,183.092,0.000 +204,255.486,185.486,0.000,220.000,150.000,774.319,185.486,0.000 +206,257.765,187.765,0.000,220.000,150.000,888.239,187.765,0.000 +208,259.924,189.924,0.000,220.000,150.000,996.201,189.924,0.000 +210,261.962,191.962,0.000,220.000,150.000,1098.077,191.962,0.000 +212,263.875,193.875,0.000,220.000,150.000,1193.741,193.875,0.000 +214,265.662,195.662,0.000,220.000,150.000,1283.078,195.662,0.000 +216,267.320,197.320,0.000,220.000,150.000,1365.977,197.320,0.000 +218,268.847,198.847,0.000,220.000,150.000,1442.340,198.847,0.000 +220,270.241,200.241,0.000,220.000,150.000,1512.071,200.241,0.000 +222,271.502,201.502,0.000,220.000,150.000,1575.089,201.502,0.000 +224,272.626,202.626,0.000,220.000,150.000,1631.311,202.626,0.000 +226,273.613,203.613,0.000,220.000,150.000,1680.675,203.613,0.000 +228,274.462,204.462,0.000,220.000,150.000,1723.117,204.462,0.000 +230,275.172,205.172,0.000,220.000,150.000,1758.585,205.172,0.000 +232,275.741,205.741,0.000,220.000,150.000,1787.038,205.741,0.000 +234,276.169,206.169,0.000,220.000,150.000,1808.441,206.169,0.000 +236,276.455,206.455,0.000,220.000,150.000,1822.767,206.455,0.000 +238,276.600,206.600,0.000,220.000,150.000,1829.999,206.600,0.000 +240,276.603,206.603,0.000,220.000,150.000,1830.127,206.603,0.000 +242,276.463,206.463,0.000,220.000,150.000,1821.768,206.463,0.000 +244,276.182,206.182,0.000,220.000,150.000,1768.646,206.182,0.000 +246,275.759,205.759,0.000,220.000,150.000,1707.237,205.759,0.000 +248,275.195,205.195,0.000,220.000,150.000,1638.172,205.195,0.000 +250,274.490,204.490,0.000,220.000,150.000,1562.135,204.490,0.000 +252,273.647,203.647,0.000,220.000,150.000,1479.867,203.647,0.000 +254,272.664,202.664,0.000,220.000,150.000,1392.148,202.664,0.000 +256,271.545,201.545,0.000,220.000,150.000,1299.806,201.545,0.000 +258,270.289,200.289,0.000,220.000,150.000,1203.705,200.289,0.000 +260,268.900,198.900,0.000,220.000,150.000,1104.737,198.900,0.000 +262,267.377,197.377,0.000,220.000,150.000,1003.832,197.377,0.000 +264,265.724,195.724,0.000,220.000,150.000,901.933,195.724,0.000 +266,263.942,193.942,0.000,220.000,150.000,800.009,193.942,0.000 +268,262.033,192.033,0.000,220.000,150.000,699.037,192.033,0.000 +270,260.000,190.000,0.000,220.000,150.000,600.000,190.000,0.000 +272,262.033,192.033,0.000,220.000,150.000,699.036,192.033,0.000 +274,263.942,193.942,0.000,220.000,150.000,800.009,193.942,0.000 +276,265.724,195.724,0.000,220.000,150.000,901.933,195.724,0.000 +278,267.377,197.377,0.000,220.000,150.000,1003.832,197.377,0.000 +280,268.900,198.900,0.000,220.000,150.000,1104.737,198.900,0.000 +282,270.289,200.289,0.000,220.000,150.000,1203.703,200.289,0.000 +284,271.545,201.545,0.000,220.000,150.000,1299.806,201.545,0.000 +286,272.664,202.664,0.000,220.000,150.000,1392.149,202.664,0.000 +288,273.647,203.647,0.000,220.000,150.000,1479.867,203.647,0.000 +290,274.490,204.490,0.000,220.000,150.000,1562.135,204.490,0.000 +292,275.195,205.195,0.000,220.000,150.000,1638.170,205.195,0.000 +294,275.759,205.759,0.000,220.000,150.000,1707.237,205.759,0.000 +296,276.182,206.182,0.000,220.000,150.000,1768.646,206.182,0.000 +298,276.463,206.463,0.000,220.000,150.000,1821.768,206.463,0.000 +300,276.603,206.603,0.000,220.000,150.000,1830.127,206.603,0.000 +302,276.600,206.600,0.000,220.000,150.000,1829.999,206.600,0.000 +304,276.455,206.455,0.000,220.000,150.000,1822.767,206.455,0.000 +306,276.169,206.169,0.000,220.000,150.000,1808.441,206.169,0.000 +308,275.741,205.741,0.000,220.000,150.000,1787.039,205.741,0.000 +310,275.172,205.172,0.000,220.000,150.000,1758.585,205.172,0.000 +312,274.462,204.462,0.000,220.000,150.000,1723.117,204.462,0.000 +314,273.613,203.613,0.000,220.000,150.000,1680.674,203.613,0.000 +316,272.626,202.626,0.000,220.000,150.000,1631.311,202.626,0.000 +318,271.502,201.502,0.000,220.000,150.000,1575.089,201.502,0.000 +320,270.241,200.241,0.000,220.000,150.000,1512.073,200.241,0.000 +322,268.847,198.847,0.000,220.000,150.000,1442.340,198.847,0.000 +324,267.320,197.320,0.000,220.000,150.000,1365.978,197.320,0.000 +326,265.662,195.662,0.000,220.000,150.000,1283.078,195.662,0.000 +328,263.875,193.875,0.000,220.000,150.000,1193.741,193.875,0.000 +330,261.962,191.962,0.000,220.000,150.000,1098.077,191.962,0.000 +332,259.924,189.924,0.000,220.000,150.000,996.201,189.924,0.000 +334,257.765,187.765,0.000,220.000,150.000,888.239,187.765,0.000 +336,255.486,185.486,0.000,220.000,150.000,774.319,185.486,0.000 +338,253.092,183.092,0.000,220.000,150.000,654.584,183.092,0.000 +340,250.584,180.584,0.000,220.000,150.000,529.179,180.584,0.000 +342,247.965,177.965,0.000,220.000,150.000,398.254,177.965,0.000 +344,245.239,175.239,0.000,220.000,150.000,261.972,175.239,0.000 +346,242.410,172.410,0.000,220.000,150.000,120.496,172.410,0.000 +348,239.480,169.480,0.000,220.000,150.000,0.000,169.480,0.000 +350,236.453,166.453,0.000,220.000,150.000,0.000,166.453,0.000 +352,233.333,163.333,0.000,220.000,150.000,0.000,163.333,0.000 +354,230.124,160.124,0.000,220.000,150.000,0.000,160.124,0.000 +356,226.829,156.829,0.000,220.000,150.000,0.000,156.829,0.000 +358,223.453,153.453,0.000,220.000,150.000,0.000,153.453,0.000 +360,220.000,150.000,0.000,220.000,150.000,0.000,150.000,0.000 diff --git a/archive/model-v2/anchor/rotation-in-flow/verdict.md b/archive/model-v2/anchor/rotation-in-flow/verdict.md new file mode 100644 index 00000000..6627172e --- /dev/null +++ b/archive/model-v2/anchor/rotation-in-flow/verdict.md @@ -0,0 +1,62 @@ +# E1 verdict — rotation-in-flow + +**Decision: lock layout-visible.** A rotated in-flow child participates in +flex by its oriented AABB (`w' = |w·cosθ| + |h·sinθ|`), box center placed +at the slot center — exactly a.md §5. Conformance **R-3** and editor +**OP-ROT-2** graduate from `POL` to `INV` on the anchor column. + +Run: 2026-07-07, `anchor-lab` @ `cargo run --bin e1` (metrics.csv is the +record; demo.html is the feel). + +## Why (measured, in triage order) + +1. **Canvas truth (#1).** The control arm produces up to **1830 px²** of + sibling overlap that _no document field expresses_ — the document says + "a list of three cards", the pixels say two of them collide. The anchor + arm never overlaps, at any angle, by construction and by measurement + (0 px² across the full sweep). This is the overlap-lie the triage's + canvas-truth answer refuses. +2. **It feels continuous (#5's fear, retired).** The plausible objection + to layout-visible rotation was jitter — layout reacting per frame. + Measured: sibling displacement is smooth and bounded by the analytic + envelope derivative (3.45 px per 2° observed vs 4.07 px bound; no + discontinuity anywhere, including through 0°/90°/180°). Rotating a card + in a list _reads as the list making room_, not as instability. +3. **The turn animates (#27).** The container breathes 56.6 px over a full + spin (peaking at θ\* = atan(h/w) ≈ 59°). For a _gesture_ this is the + point — the list visibly negotiates space. For _pure motion_ (a card + spinning forever) breathing would be noise — which is precisely the + spec's existing two-lane rule (§5): **motion rotation targets a lens + channel; only intent rotation is layout-visible.** E1 turns that rule + from doctrine into a measured necessity: the experiment _confirms_ the + two-lane split rather than weakening layout-visible. +4. **No gesture tax.** Rotation stays a single-field write in flow in both + arms (asserted every step of the sweep). Layout-visibility costs zero + write amplification; the reflow is resolver work, not document work. +5. **Predictability (#17).** The arithmetic an agent needs is one formula + (`|w·cosθ|+|h·sinθ|`); the E3 cold-prediction probe includes + rotated-in-flow cases — see + [`../text-ir/verdict.md`](../text-ir/verdict.md) for the scored + result feeding this clause. + +## Declared consequences (into the spec at phase 3) + +- R-3 → INV: "siblings make room for the rotated AABB; contribution is + computed from resolved size only, never position" (single-pass safety + stays load-bearing). +- OP-ROT-2 → INV: rotate-in-flow = 1 write; the editor previews reflow + live during the gesture. +- **Envelope-peak note for the editor layer**: the AABB maximum is at + θ\* = atan(h/w), not 90° — a card is _widest_ mid-turn. HUD affordances + (e.g. snapping readouts) should surface the envelope, or users will + read the pre-90° maximum as a bug. Recorded as an editor.md follow-up, + not a model change. +- Fixed-width containers don't breathe (measured identical displacement, + zero container change) — breathing is a hug-container phenomenon only. + +## What would have reversed this + +If the sweep had shown displacement discontinuities (slot reordering +mid-turn), unbounded per-degree movement, or gesture write amplification, +the tilt would have flipped to visual-only + an explicit opt-in switch. +None occurred. diff --git a/archive/model-v2/anchor/shape-points-scale/README.md b/archive/model-v2/anchor/shape-points-scale/README.md new file mode 100644 index 00000000..9f37dfdd --- /dev/null +++ b/archive/model-v2/anchor/shape-points-scale/README.md @@ -0,0 +1,168 @@ +# E7 — shape definition vs layout; the points problem; the two scales + +Owner question (2026-07-07, post-run): did the model solve the +**XYWH-vs-shape-definition** semantic problem? What about the **points +problem** — when layout changes the box, do points update? And when +content "scales", is it **geometry-scaled or post-render-scaled** — and +what happens to an **image fill**? (Hint given: Framer/Sketch solve this +with an explicit ``/``-like container node.) + +Method: 5-agent research fan-out (Framer runtime source from the shipped +npm package, Sketch file-format + Athens docs, Figma plugin API + .fig +format, SVG2/CSSWG spec, and a grounding pass over the current Grida +engine/editor). Full structured facts in the workflow record; the +decisive ones are quoted below. + +## 1. XYWH vs shape definition — SOLVED (by design, lab-proven) + +The anchor answer is the _inversion_: a shape has no intrinsic geometry +the box must chase — **geometry is a function of the box** +(`ShapeDescriptor`, size-free, evaluated at the resolved box; a.md §3.2, +problem P3). W/H are ordinary layout participants; there is nothing for +layout to fight. + +- This is also what the current engine already does for primitives + (verified: `to_shape()` rebuilds from `size` each call; fbs + `CanonicalLayerShape` "intentionally does NOT encode size") — the model + ratifies the direction rather than inventing one. +- It is also Figma's answer (verified via plugin API/.fig: star = + `pointCount` + `innerRadius` _ratio_ + size; `cornerRadius` in px, so a + non-uniform resize re-evaluates the outline and arcs stay circular). +- Lab-proven for rect/ellipse/line through flex/stretch/grow. + +## 2. The points problem — industry-convergent answer; a.md needs E-A9 + +Three independent systems converged on the same structure, and none of +them rewrites points on resize: + +| system | storage | resize | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | +| Sketch | curve points normalized 0–1 relative to the frame — "so they don't require any update in case the frame of the layer changes"; the format has **no transform property at all** | frame write only | +| Figma | `.fig` vectorNetwork stored against `vectorData.normalizedSize`; render maps by `size / normalizedSize`; the plugin API _presents_ materialized coordinates | size write only | +| SVG | `viewBox` — a render-time coordinate-system transform | viewport write only | +| **Grida today** | absolute vector network; the TS reducer **bakes** the resize into every vertex + tangent (`VectorNetworkEditor.scale`) | **full point rewrite** — the outlier | + +The bake is exactly the class of behavior the model exists to kill: +write amplification (a drag writes N vertices), CRDT hostility +(resize ∥ vertex-edit conflicts on every vertex), float accumulation +over repeated resizes. + +**E-A9 (spec delta):** `vector`'s box source changes from +"measured (network bounds; no size intent)" to **declared-with-reference- +space**: points live in a reference space; the node carries ordinary +size intent; resolution maps points by `resolved_size / reference_size` +(render-time geometry scale — never a document write). Consequences: +resize = 1–2 field writes like every other kind; vectors under +layout-imposed extents (grow/stretch) become _defined_ for free (the +same mapping) — closing a real hole in a.md §4/§6; T-5 still holds +(vertex edits update the reference bounds); the current editor's +point-bake becomes an X-SELF break with a migration +(bounds-normalize once at import). +Predictability cost, mitigated: the text IR (an agent projection) can +present materialized coordinates, exactly as Figma's plugin API does +over its normalized storage. + +### 2b. Reference: our own io-figma already implements the mapping + +The owner's hint checks out in-repo (studied 2026-07-07, `io-figma`): + +- **Logic**: `packages/grida-canvas-io-figma/lib.ts` — + `scaleVectorNetworkFromNormalizedSize()` maps blob coordinates by + `sx = size.x / normalizedSize.x` (etc.), applied to **both vertex + positions and segment tangents**, at import time. +- **Proof Figma never rewrites the blob on resize**: the test + `__tests__/iofigma.kiwi.vector-network.test.ts` deliberately selects a + real `.fig` fixture node **where `normalizedSize != size`** — such + nodes exist in shipped files precisely because resize only writes + `NodeChange.size` and leaves the blob untouched (the owner's + observation, fixture-verified). +- **Notes**: `docs/wg/feat-fig/glossary/fig.kiwi.md` + §"Vector network coordinate space (observed)" documents the mapping, + the mis-render consequence of reading blobs "as-is", and one caveat + that **refines E-A9**: _some blobs have non-zero bbox origins + (minX/minY ≠ 0), so an additional translation may be needed beyond + pure scaling_ — i.e. the reference space is a **rect, not a size**. + E-A9 should either store a reference _rect_ or normalize the origin + at write time; a bare `reference_size` under-specifies. +- **Polygon/star: the mechanism does NOT apply, because there are no + points.** In the kiwi data, STAR/POLYGON are purely parametric — + `count`, `starInnerScale`, `cornerRadius`, `size` (lib.ts `star()` + uses `nc.count`/`nc.starInnerScale`; polygon `nc.count ?? 3`). No + stored outline exists to be in any coordinate space; resize + re-derives. Only VECTOR carries the normalized blob. +- **A tidy asymmetry worth naming**: the _editable_ network lives in + reference space (intent), while the _pre-baked_ `fillGeometry`/ + `strokeGeometry` command blobs — when present — are derived, size-space + geometry (our converter prefers them and applies no normalizedSize + scaling). Figma's own file thus mirrors the model's tier split: + intent in a stable space, resolved output baked separately. +- **The caveat the owner predicted** is real and documented: any tool + editing blob data directly must know the mapping (the Plugin API + hides it by materializing coordinates into current-size space). For + anchor/E-A9 the same division of labor applies: the binary format + keeps the reference space (round-trip- and CRDT-friendly — resize + ∥ vertex-edit touch disjoint fields); the **text IR materializes**, + so agents read/write real coordinates. + +## 3. Scaled how — geometry-at-render, never raster; and TWO scales + +- SVG's viewBox stretch is **geometry scaling at render** (a live + coordinate transform; content re-rasterizes crisp at device + resolution). Framer is the cautionary partial exception: its editor + canvas post-render CSS-scales the Graphic for drag performance, and + small SVGs are flattened to a stretched `background-image` data-URI. +- Every design tool refuses SVG's stroke default for plain resize: + strokes stay px-constant (Sketch: "strokes… stay the same when you + resize"; Figma: `resize()` never touches strokeWeight; Grida today: + same). The **anchor model has this by construction** — stroke width is + a style scalar that never enters the geometry mapping; + `vector-effect: non-scaling-stroke` semantics without the attribute. +- The second scale — "scale it like a picture" — is everywhere an + **explicit, separate gesture** that rewrites parameters: Figma + `rescale()`/K (strokes, blurs, font size), Sketch K (border, shadow, + text), and Grida already ships it (`parametric_scale`, K). The model + keeps this as an op-layer _bake_, not a stored mode. For a _retained_ + picture-scale (rare), the structural home is a `lens` `Scale` op — + under it strokes and everything else scale visually, which is exactly + SVG/Framer-Graphic semantics, quarantined and opt-in. + +## 4. Image fills — re-fit at the box, scaled only inside a lens + +Consensus (Figma scaleModes; current Grida verified: the image matrix is +recomputed from `container_size` at paint time, `BoxFit` + +box-relative `Transform` + `Tile`): a fill is **paint-tier state +re-evaluated at the resolved box**, never baked into geometry. Framer's +Graphic does the opposite (`objectBoundingBox` pattern, +`preserveAspectRatio="none"` → fills stretch and distort with the box) — +and that distortion is among its documented user complaints. +**E-A10 (rule, locked):** paints evaluate at the resolved box per their +fit mode; the only way an image fill "scales like a picture" is through +a lens op — same quarantine as every other picture-scale. + +## 5. The `` container — deliberately not required + +Both hinted systems make proportional graphics a _container mode_: +Framer's Graphic layer (opaque SVG string, stretch semantics) and +Sketch's Athens **Graphics** container (2025.1 — "contents resize +proportionally", no pinning inside, pasted SVG becomes one). The +evidence says the container works but its _mandatoriness_ hurts: Framer +users buy SVG code components to escape Graphic resize behavior, K was +added (Feb 2025) because plain resize couldn't scale styles, and +community threads complain about px-only sizing and aspect-lock fights. + +Anchor inverts the ergonomics: the **default** world is the layout world +— shapes are first-class citizens with parametric geometry and vectors +carry the normalized mapping _per leaf_, so no container is ever needed +for the common case (including "resize an imported icon group": +per-child size writes, no point rewrites, no wrapper). The proportional +world still exists, but as the opt-in escape (`lens` scale / wrapper +frame), not as a prerequisite node users must remember to create. + +## Verdict + +Question 1 was already solved and proven; question 2 exposes the one +real correction (**E-A9** — adopt the Sketch/Figma reference-space +mapping for `vector`, retiring today's point-bake); questions 3–4 were +implicitly right and are now locked as explicit rules (**E-A10**, two +scales doctrine); the Graphics-container path is rejected as a +requirement with evidence, kept as the lens escape. diff --git a/archive/model-v2/anchor/spike-readout.md b/archive/model-v2/anchor/spike-readout.md new file mode 100644 index 00000000..e9331d47 --- /dev/null +++ b/archive/model-v2/anchor/spike-readout.md @@ -0,0 +1,99 @@ +# The feel spike — read-out (archive ledger E10) + +2026-07-07. Built in one session on the lab (which gained the arena/SOA +storage, `pick`, and `delete` — 100 tests green). Self-verified via +`--shot` headless renders and a 6 s window smoke-launch; the FEEL half +of the acceptance is the owner's (README checklist). + +## Numbers + +Resolver bench (E4, median of 11, release) — the arena+SOA refactor +alone, before the spike even paints: + +| scene | map store | arena+SOA | Δ | +| ------------ | --------- | --------- | ------------------- | +| flat 1,000 | 0.753 ms | 0.097 ms | 7.8× | +| flat 10,000 | 5.462 ms | 0.473 ms | 11.5× | +| mixed 10,000 | 8.534 ms | 3.005 ms | 2.8× | +| flex ~10,000 | 24.373 ms | 19.341 ms | 1.26× (Taffy-bound) | + +Spike frame bench (`--bench`, raster, resolve + skia paint): + +| scene | nodes | resolve | paint | frame | +| ------------- | ------ | -------- | -------- | ------- | +| starter scene | 18 | 0.008 ms | 0.163 ms | 0.17 ms | +| flat 100 | 101 | 0.004 ms | 0.183 ms | 0.19 ms | +| flat 1,000 | 1,001 | 0.034 ms | 1.248 ms | 1.28 ms | +| flat 10,000 | 10,001 | 0.345 ms | 8.674 ms | 9.02 ms | + +**Verdict on the thesis:** resolve-per-frame holds with an order of +magnitude of headroom — at editor scales the frame is PAINT-bound, not +resolve-bound. The incremental invalidator is an optimization for +later, not a load-bearing wall. (This is the inverse of the legacy +architecture's assumption, and now it has numbers.) + +## Verified headless (committed-adjacent in [shots/](./shots); reproduce with --shot) + +- `default`: the row makes room for the 20° card; HUD reads + "70×110 basis · 103×127 envelope · 20.0°" (E-A7); badge end-pinned; + bar spanned; lens child rotated in the paint lane; hug breathing. +- `crosszero`: card.a dragged 50 px past its fixed right edge → + 50×110, `flip_x=true`, row reflowed (E-A14 re-target, no negative + extent stored). +- `ungroup`: chips baked to the artboard, world-exact. +- Window smoke-launch: 6 s, exit clean (GL + skia + egui painter + + event loop healthy). + +## Frictions found by hand (the spike's real yield) + +1. **Rotated edge-resize steers in parent axes.** `resize_drag` is + parent-axis by design; on a rotated node the handle follows the + cursor's parent-X, not the local edge normal. Usable, but the + local-axis gesture (project cursor onto the local axis, then + re-target) belongs in editor.md as the real spec — follow-up, not a + model change (R-4 commute already guarantees the write side). +2. **Text: measured-vs-rendered mismatch is visible** (lab stub vs + default typeface). Expected — this is open DEC-4/B-1 rendered + honest. The moment a real shaper lands, the measure closure seam in + `resolve.rs` is where it plugs. +3. **Hover-pick every pointer-move re-resolves.** At spike scale it's + free (8 µs); at product scale the resolved tier of the LAST frame is + already correct for picking — reuse it (the SOA columns make that a + borrow, not a copy). Noted for the migration. +4. **skia-safe 0.93 removed mutable `Path`** — `PathBuilder` + + `snapshot()` (the engine already does this; the textbook records it + so the migration doesn't rediscover it). +5. **egui 0.35 API drift** (`Panel` not `SidePanel`, `run_ui`, + `egui_wants_pointer_input`) — grida_editor's shell is the reliable + reference, upstream docs are not. + +## Register feedback + +- Nothing falsified. DEC-1's pop reproduces exactly as the register + describes (rotate card.grow and watch it snap at the first degree — + with the report badge visible in the panel). +- DEC-9's flip arm feels RIGHT in the hand (drag through zero and + back): supports the owner lean (true flip) with gesture evidence. +- E-A13 untouched by the spike (group pass-through not exercised — + the starter groups are Start-pinned free-context). + +## What this cost + +Lab: +arena/SOA refactor (semantic-equality PartialEq, structural +APIs), +`pick.rs`, +`ops::delete` — net +8 tests (100 total). +Spike: ~1,900 lines across 9 files, one session, skia reused from the +repo's shared target dir (first full build ≈ 2.5 min). + +## Addendum — DEC-0 flip (same day) + +After the read-out, DEC-0 second-locked to **visual-only (CSS framing, +CSS-pure sizing)** — owner framing, correcting a mis-recorded first +lock. The gated spec review ran: normative rules in +`../../model-v2/anchor/dec0-visual-only.md` (the V-4 group-box fork was real UB until +decided — sizing-tier union chosen); lab default flipped; +`tests/visual_only.rs` added (14 tests; suite now **114**); shots above +regenerated under the new default (the rot45 shot now shows overlap as +correct behavior and the hug frame NOT breathing — compare the fork +demo's right panel). The E-A11 pop noted in "register feedback" is +n/a under the new default — the configuration it popped in no longer +exists. diff --git a/archive/model-v2/anchor/svg-corpus/README.md b/archive/model-v2/anchor/svg-corpus/README.md new file mode 100644 index 00000000..fc5277e5 --- /dev/null +++ b/archive/model-v2/anchor/svg-corpus/README.md @@ -0,0 +1,47 @@ +# E5 — SVG import corpus measurement + +**Question.** Triage amendment 5 makes SVG import ≈100% a hard +requirement but left the carrying mechanism open (lens-quarantine default +vs declared degradation), to be decided by corpus measurement. How much +real SVG content actually exceeds the anchor header's native vocabulary +(bindings + size + rotation)? + +**Method.** [`../lab/src/bin/e5scan.rs`](../lab/src/bin/e5scan.rs) scans +every `transform` / `gradientTransform` / `patternTransform` attribute, +composes each transform list into a 2×3 matrix, and classifies by the +decomposition `M = R(θ)·[sx m; 0 sy]`: identity/translate → bindings; +rotate → header rotation; scale (uniform or not, `m≈0`) → folds into +size; `det<0` → single-axis mirror (flip class); `|m|>ε` → true shear. +Geometry and paint transforms are tallied separately — a gradient's +matrix lives in the _paint_, never in node geometry. + +**Corpus** (2026-07-07, repo-local): `fixtures/local` (6,626 wild +real-world files), `fixtures/test-svg` (37), `third_party/usvg` (62), +`packages/grida-canvas-sdk-render-figma` (333 Figma-derived), +`editor/public` (24), `public/slides-templates` (56) — +**7,138 files, 1,003,787 transforms** (613,084 geometry + 390,703 paint). + +## Results + +Geometry transforms by class: + +| class | count | share | lands where | +| ----------------------------- | ----------- | --------- | ------------------- | +| identity / translate | 55,529 | 9.1% | bindings | +| rotate | 7,989 | 1.3% | header `rotation` | +| scale, uniform | 121,268 | 19.8% | size fold | +| scale, non-uniform (no shear) | 262,607 | 42.8% | size fold | +| **flip (det < 0)** | **159,872** | **26.1%** | ← the finding | +| true shear | 5,809 | **0.95%** | lens | +| unparsable | 10 | 0.002% | lens (conservative) | + +Per file: **57.8%** import fully natively as-is; **+23.9%** are blocked +_only_ by flips; **18.3%** contain ≥1 geometry shear (wild corpus 19.7%; +curated corpora 0–8.1%). Explicit `skewX/skewY` appears in only 44 +files — shear arrives almost entirely as baked `matrix()`. + +Paint transforms: 390,703 total, 17,872 with shear — none of them a node +problem (the paint model already carries full matrices). Splitting these +out cut the apparent "needs lens" file rate from 32.5% to 18.3%. + +The verdict lives in [`verdict.md`](./verdict.md). diff --git a/archive/model-v2/anchor/svg-corpus/verdict.md b/archive/model-v2/anchor/svg-corpus/verdict.md new file mode 100644 index 00000000..e9e2f60f --- /dev/null +++ b/archive/model-v2/anchor/svg-corpus/verdict.md @@ -0,0 +1,49 @@ +# E5 verdict — SVG import mechanism + +**Decision: quarantine confirmed for shear; and the corpus forces one +spec amendment — flip must be native.** Amendment 5's ≈100% requirement +is achievable with zero degradation. + +## The four rulings + +1. **Flip goes native (spec amendment proposal, measured).** Single-axis + mirrors are **26.1% of all geometry transforms** (159,872 occurrences; + sole blocker for 23.9% of files) — the standard y-up→y-down and + mirrored-asset idioms of real SVG. A quarantine that fires on a + quarter of wild files is not a quarantine. Proposal for phase 3: two + header booleans `flip_x` / `flip_y` (applied about the box center, + after rotation semantics are unchanged) — scalar intent, CRDT-atomic, + animatable, and exactly what Figma exposes. This answers conformance + **R-E5** ("declared representation for negative-determinant + transforms"). With native flip, per-file native coverage rises from + 57.8% → **81.7%**. +2. **Lens is the right home for true shear.** Genuine shear is **0.95%** + of geometry transforms — per-transform it is exactly the exceptional + class the lens was designed for (H8: wrap, never silent loss). It + appears in 18.3% of _wild_ files (0–8% in curated corpora), typically + on a handful of nodes per file — an acceptable wrap rate for content + that genuinely is sheared. Degradation is unnecessary: only 10 of a + million transforms were unparsable (wrapped conservatively). +3. **Paint transforms never touch the node model.** 390k gradient/pattern + matrices (including all 17,872 paint shears) belong to the paint, + which already carries a full 2×3 in Grida. Importer rule, locked: + `gradientTransform`/`patternTransform` → paint transform, never a + lens. (Skipping this split overstates the lens rate by 14 points — + the measurement that justified the rule.) +4. **X-SVG-1/2/3 verdicts confirmed as written**: translate/rotate/scale + lists import losslessly into bindings/rotation/size (73% of geometry + transforms immediately, no structure added); skew/matrix = "Y-with- + structure" via lens; `x/y` attr + `translate()` compose into one + binding offset at import. + +## What this cost the model + +One honest widening: the header grows two booleans. The alternative — +lens-wrapping a quarter of imported files — would have diluted H5's +"matrices are tier-2 only" story far more than two flip bits do. The +scale fold (62.6% of transforms landing in `w`/`h`) confirms the +size-free shape-descriptor design (`CanonicalLayerShape` direction) pulls +its weight in practice. + +Reproduce: `cargo run --release --bin e5scan -- ` from +`../lab`. diff --git a/archive/model-v2/anchor/text-ir/README.md b/archive/model-v2/anchor/text-ir/README.md new file mode 100644 index 00000000..40e7bb06 --- /dev/null +++ b/archive/model-v2/anchor/text-ir/README.md @@ -0,0 +1,43 @@ +# E3 — the agent text IR + +**Question.** Triage amendment 3 makes an XML-ish textual projection a +first-class product surface whose audience is LLMs: an agent must be able +to _write_ a document and _predict its resolved geometry mentally_ +(triage #17 "critical", #20 "the agent should be tricked it's writing a +file"). Is the anchor model actually mentally simulable from a spec alone? + +**Deliverables here.** + +- [`grammar.md`](./grammar.md) — the pocket grammar + hand-resolution + semantics (the exact text given to probe agents; also the seed of the + phase-3 text-IR spec). +- Reference implementation: parser + canonical printer in + [`../lab/src/textir.rs`](../lab/src/textir.rs), with round-trip tests + (`parse ∘ print` fixpoint; a.md §7 quartet expressed and resolved — + `../lab/tests/textir_suite.rs`). +- [`probes/`](./probes) — six probe documents: bindings/span (p1), flex + grow + cross alignment (p2), hug + text wrap + stretch (p3), free + rotation AABB (p4), **rotated-in-flow** under the E1-locked rule (p5), + group union + origin placement (p6). +- [`truth.txt`](./truth.txt) — resolver ground truth + (`cargo run --bin e3 -- truth`). +- [`predictions/`](./predictions) — raw agent outputs, scored by + `cargo run --bin e3 -- score ` (tolerance 0.5 px). + +**Probe protocol.** Three fresh agents (two Fable-class, one Haiku-class +as a small-model control), given ONLY the grammar text and the six +documents inline. No tools, no resolver, no prior context from this +workbench. Output: `file,node,box|aabb,x,y,w,h` lines for every named +node (22 lines). Truth emits a world-AABB line only for rotated, +**non-group** nodes; groups report a derived box but no AABB +(unexercised here — no probe rotates a group). + +**Results (2026-07-07).** + +| agent | score | failures | +| ------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fable-a | **22/22** | — | +| fable-b | **22/22** | — | +| haiku-c | 17/22 | p3: counted 15 chars as 14 (wrap off-by-one) and stretched a fixed-width shape against the stated rule; p5: stated the slot-center rule correctly, then placed the box at slot _start_ | + +The verdict lives in [`verdict.md`](./verdict.md). diff --git a/archive/model-v2/anchor/text-ir/grammar.md b/archive/model-v2/anchor/text-ir/grammar.md new file mode 100644 index 00000000..9424cc82 --- /dev/null +++ b/archive/model-v2/anchor/text-ir/grammar.md @@ -0,0 +1,153 @@ +# The anchor text IR — pocket grammar & resolution semantics + +> Historical note: this freezes the E3 TextIr element and attribute vocabulary. +> ` is not canonical n0 XML; use "), + "stroke" => { + return err( + " inside is not supported until run stroke geometry matches the production model", + ) + } + tag if is_html_inline_semantic_tag(tag) => { + return html_inline_semantic_error(tag) + } + _ => { + return err(format!( + "<{child_tag}> is not allowed inside ; only a leading property and character data are valid" + )) + } + } + } + Ok(Event::End(end)) => { + let tag = String::from_utf8_lossy(end.name().as_ref()).to_string(); + if tag != "tspan" { + return err(format!("mismatched end tag in ")); + } + if text.is_empty() { + return err(" must contain at least one character"); + } + return Ok(PendingTextSegment { text, style, fills }); + } + Ok(Event::Comment(_)) => { + if !fill_child_seen { + event_before_fill = true; + } + } + Ok(Event::CData(_) | Event::Decl(_) | Event::PI(_) | Event::DocType(_)) => { + return err("unsupported XML event in ") + } + } + } +} + +struct Pending { + id: NodeId, + tag: String, + text_content: String, + text_segments: Vec, + default_text_style: Option, + is_text: bool, + fill_seen: bool, + stroke_seen: bool, + legacy_fill_seen: bool, + content_started: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Dialect { + TextIr, + N0Xml, +} + +pub fn parse(input: &str) -> Result { + parse_with_dialect(input, Dialect::TextIr) +} + +/// Shared parser core for the first-class `.n0.xml` surface. Kept +/// crate-private so the historical [`parse`] contract remains exactly the +/// experiment grammar while `n0_xml` owns its public error vocabulary. +pub(crate) fn parse_n0_xml(input: &str) -> Result { + parse_with_dialect(input, Dialect::N0Xml) +} + +fn parse_with_dialect(input: &str, dialect: Dialect) -> Result { + let mut reader = Reader::from_str(input); + let n0_xml = dialect == Dialect::N0Xml; + // Draft 0 preserves authored whitespace inside . Historical TextIr + // keeps its experiment-era trimming behavior for compatibility. + reader.config_mut().trim_text(!n0_xml); + + let mut nodes: BTreeMap = BTreeMap::new(); + let mut stack: Vec = vec![]; + let mut next_id: NodeId = 0; + let mut root: Option = None; + let mut render_root: Option = None; + let mut envelope_open = false; + let mut envelope_closed = false; + let mut declaration_seen = false; + let mut pre_declaration_content = false; + + if n0_xml { + // `.n0.xml`'s envelope is structural, while the model's root is + // the canonical viewport-spanning frame. The one authored render + // root is attached beneath it rather than replacing it. + let root_doc = DocBuilder::new().build(); + let root_id = root_doc.root; + nodes.insert(root_id, root_doc.get(root_id).clone()); + next_id = 1; + root = Some(root_id); + } + + loop { + match reader.read_event() { + Err(e) => return err(format!("xml: {e}")), + Ok(Event::Eof) => break, + Ok(ev @ (Event::Start(_) | Event::Empty(_))) => { + let is_empty = matches!(ev, Event::Empty(_)); + let el = match ev { + Event::Start(e) | Event::Empty(e) => e, + _ => unreachable!(), + }; + let tag = String::from_utf8_lossy(el.name().as_ref()).to_string(); + + if n0_xml && tag == "grida" { + if is_empty { + return err(" must contain exactly one render root"); + } + if envelope_open || envelope_closed || !stack.is_empty() { + return err(" must be the single document envelope"); + } + let mut version: Option = None; + for attr in el.attributes() { + let attr = attr.map_err(|e| ParseError(format!("attr: {e}")))?; + let key = String::from_utf8_lossy(attr.key.as_ref()).to_string(); + let val = attr + .unescape_value() + .map_err(|e| ParseError(format!("attr value: {e}")))? + .to_string(); + if key != "version" { + return err(format!("unknown attribute `{key}` on ")); + } + if version.replace(val).is_some() { + return err("duplicate `version` on "); + } + } + match version.as_deref() { + Some("0") => {} + Some(v) => return err(format!("unsupported version `{v}`")), + None => return err(" requires version=\"0\""), + } + envelope_open = true; + continue; + } + + if n0_xml && (!envelope_open || envelope_closed) { + return err(format!("<{tag}> must be inside ")); + } + if n0_xml && tag == "tspan" { + let parent = stack.last_mut().ok_or_else(|| { + ParseError(" must be a direct child of ".into()) + })?; + if !parent.is_text { + return err(format!( + " must be a direct child of , not <{}>", + parent.tag + )); + } + if is_empty { + return err(" must contain at least one character"); + } + let inherited_style = parent + .default_text_style + .expect("text pending state carries its default style"); + if !parent.text_content.is_empty() { + parent.text_segments.push(PendingTextSegment { + text: std::mem::take(&mut parent.text_content), + style: inherited_style, + fills: None, + }); + } + parent.content_started = true; + let segment = parse_tspan(&mut reader, &el, inherited_style)?; + stack + .last_mut() + .expect("parent remains open") + .text_segments + .push(segment); + continue; + } + if n0_xml + && stack.last().is_some_and(|parent| parent.is_text) + && is_html_inline_semantic_tag(&tag) + { + return html_inline_semantic_error(&tag); + } + if n0_xml && tag == "span" && stack.last().is_some_and(|parent| parent.is_text) { + return err(" is not canonical n0 XML; use "); + } + if n0_xml && matches!(tag.as_str(), "fill" | "stroke") { + let attributes = collect_attributes(&el)?; + let parent = stack.last().ok_or_else(|| { + ParseError(format!( + "<{tag}> must be a direct child of a paintable node" + )) + })?; + if parent.content_started { + return err(format!( + "<{tag}> must appear before content or scene children in <{}>", + parent.tag + )); + } + let parent_id = parent.id; + let parent_tag = parent.tag.clone(); + let parent_node = nodes.get(&parent_id).unwrap(); + let payload = parent_node.payload.clone(); + let corner_smoothing = parent_node.corner_smoothing; + + // Whitespace before or between leading text properties is + // formatting, not character content. The same buffer is + // retained when no later property appears, preserving all + // authored text after the final property. + if parent.is_text { + debug_assert!(parent.text_content.trim().is_empty()); + stack.last_mut().unwrap().text_content.clear(); + } + + match tag.as_str() { + "fill" => { + let parent = stack.last().expect("parent remains open"); + if parent.fill_seen { + return err(format!("duplicate on <{}>", parent.tag)); + } + if parent.stroke_seen { + return err(format!( + " must precede elements in <{}>", + parent.tag + )); + } + if parent.legacy_fill_seen { + return err(format!( + "<{}> cannot use both the `fill` attribute and ", + parent.tag + )); + } + if !supports_fill(&payload) { + return err(format!(" is not valid on <{parent_tag}>")); + } + reject_unknown_attribute(&attributes, "fill")?; + let paints = if is_empty { + Paints::default() + } else { + parse_paint_channel(&mut reader, "fill")? + }; + nodes.get_mut(&parent_id).unwrap().fills = paints; + stack.last_mut().unwrap().fill_seen = true; + } + "stroke" => { + if matches!(payload, Payload::Group | Payload::Lens { .. }) { + return err(format!(" is not valid on <{parent_tag}>")); + } + let paints = if is_empty { + Paints::default() + } else { + parse_paint_channel(&mut reader, "stroke")? + }; + let stroke = + parse_stroke(attributes, &payload, corner_smoothing, paints)?; + if stroke.paints.is_empty() && stroke.geometry_is_default_for(&payload) + { + return err(format!( + "default empty (empty stroke) on <{parent_tag}> is indistinguishable from omission" + )); + } + nodes.get_mut(&parent_id).unwrap().strokes.push(stroke); + stack.last_mut().unwrap().stroke_seen = true; + } + _ => unreachable!(), + } + continue; + } + if n0_xml && matches!(tag.as_str(), "fills" | "strokes") { + return err(format!( + "plural <{tag}> is not Draft 0; use singular <{}>", + if tag == "fills" { "fill" } else { "stroke" } + )); + } + if n0_xml && is_legacy_gradient_tag(&tag) { + return err( + "kind-specific gradient tags are not Draft 0; use ", + ); + } + if n0_xml && is_typed_paint_tag(&tag) { + if tag == "image" { + return err( + "scene is not supported in Draft 0; place directly inside or for an image paint", + ); + } + return err(format!( + "<{tag}> is a paint and must be a direct child of or " + )); + } + if n0_xml && tag == "stop" { + return err(" must be a direct child of a gradient paint"); + } + if n0_xml && tag == "frame" { + return err(" belongs to historical textir; use "); + } + if n0_xml && tag == "shape" { + return err( + " is reserved in Draft 0; use , , , or ", + ); + } + let is_authored_root = n0_xml && stack.is_empty() && render_root.is_none(); + if is_authored_root && tag != "container" { + return err(format!( + "the authored render root must be , found <{tag}>" + )); + } + if stack.last().is_some_and(|parent| { + !nodes + .get(&parent.id) + .expect("open parent exists") + .payload + .accepts_children() + }) { + let parent = stack.last().expect("checked above"); + return err(format!( + "<{}> cannot contain child elements; use for local composition", + parent.tag + )); + } + if let Some(parent) = stack.last_mut() { + parent.content_started = true; + } + + let (node_tag, direct_shape_kind) = match (dialect, tag.as_str()) { + (Dialect::N0Xml, "container") => ("frame", None), + (Dialect::N0Xml, "rect") => ("shape", Some(ShapeDesc::Rect)), + (Dialect::N0Xml, "ellipse") => ("shape", Some(ShapeDesc::Ellipse)), + (Dialect::N0Xml, "line") => ("shape", Some(ShapeDesc::Line)), + (Dialect::N0Xml, "path") => ("path", None), + _ => (tag.as_str(), None), + }; + let mut header = Header::new(SizeIntent::Auto, SizeIntent::Auto); + let mut layout = LayoutBehavior::default(); + let mut shape_kind: Option = direct_shape_kind; + let mut path_d: Option = None; + let mut path_fill_rule = FillRule::NonZero; + let mut font_size = 16.0f32; + let mut font_weight = TextStyleRec::DEFAULT_FONT_WEIGHT; + let mut font_style_italic = false; + let mut lens_ops: Vec = vec![]; + let mut clips = false; + let mut corner_radius = RectangularCornerRadius::default(); + let mut corner_smoothing = CornerSmoothing::default(); + let mut legacy_fill: Option = None; + let mut width_seen = false; + let mut height_seen = false; + let mut x_seen = false; + let mut y_seen = false; + let mut flow_seen = false; + let mut grow_seen = false; + let mut align_seen = false; + let mut box_constraint_seen = false; + let mut aspect_seen = false; + let mut corner_radius_seen = false; + let mut corner_smoothing_seen = false; + let mut frame_only_attr: Option = None; + let mut flex_only_attr: Option = None; + let mut shape_only_attr: Option = None; + let mut text_only_attr: Option = None; + let mut lens_only_attr: Option = None; + let mut path_only_attr: Option = None; + let strict = n0_xml; + + let mut source_attributes = el.attributes(); + source_attributes.with_checks(false); + let mut seen_source_attributes = BTreeSet::new(); + for attr in source_attributes { + let attr = attr.map_err(|e| ParseError(format!("attr: {e}")))?; + let key = String::from_utf8_lossy(attr.key.as_ref()).to_string(); + if !seen_source_attributes.insert(key.clone()) { + return err(format!("duplicate `{key}` on <{tag}>")); + } + let val = attr + .unescape_value() + .map_err(|e| ParseError(format!("attr value: {e}")))? + .to_string(); + match key.as_str() { + "name" => header.name = Some(val), + "x" => { + x_seen = true; + header.x = parse_binding(&val, "x")?; + } + "y" => { + y_seen = true; + header.y = parse_binding(&val, "y")?; + } + "w" if !n0_xml => { + if width_seen { + return err(format!("duplicate width attribute on <{tag}>")); + } + width_seen = true; + header.width = parse_size(&val, "width", false)?; + } + "width" if n0_xml => { + if width_seen { + return err(format!("duplicate width attribute on <{tag}>")); + } + width_seen = true; + header.width = parse_size(&val, "width", true)?; + } + "h" if !n0_xml => { + if height_seen { + return err(format!("duplicate height attribute on <{tag}>")); + } + height_seen = true; + header.height = parse_size(&val, "height", false)?; + } + "height" if n0_xml => { + if height_seen { + return err(format!("duplicate height attribute on <{tag}>")); + } + height_seen = true; + header.height = parse_size(&val, "height", true)?; + } + "min-w" if !n0_xml => { + header.min_width = Some(parse_non_negative(&val, "min-w", false)?) + } + "min-width" if n0_xml => { + box_constraint_seen = true; + header.min_width = Some(parse_non_negative(&val, "min-width", true)?) + } + "max-w" if !n0_xml => { + header.max_width = Some(parse_non_negative(&val, "max-w", false)?) + } + "max-width" if n0_xml => { + box_constraint_seen = true; + header.max_width = Some(parse_non_negative(&val, "max-width", true)?) + } + "min-h" if !n0_xml => { + header.min_height = Some(parse_non_negative(&val, "min-h", false)?) + } + "min-height" if n0_xml => { + box_constraint_seen = true; + header.min_height = Some(parse_non_negative(&val, "min-height", true)?) + } + "max-h" if !n0_xml => { + header.max_height = Some(parse_non_negative(&val, "max-h", false)?) + } + "max-height" if n0_xml => { + box_constraint_seen = true; + header.max_height = Some(parse_non_negative(&val, "max-height", true)?) + } + "aspect" if !n0_xml => { + let (a, b) = val + .split_once(':') + .ok_or_else(|| ParseError("aspect needs `w:h`".into()))?; + header.aspect_ratio = + Some((parse_num(a, "aspect")?, parse_num(b, "aspect")?)); + } + "aspect-ratio" if n0_xml => { + box_constraint_seen = true; + aspect_seen = true; + let (a, b) = val + .split_once(':') + .ok_or_else(|| ParseError("aspect-ratio needs `w:h`".into()))?; + header.aspect_ratio = Some(( + parse_positive(a, "aspect-ratio", true)?, + parse_positive(b, "aspect-ratio", true)?, + )); + } + "corner-radius" if n0_xml => { + if corner_radius_seen { + return err(format!( + "duplicate corner-radius attribute on <{tag}>" + )); + } + corner_radius_seen = true; + corner_radius = parse_corner_radius(&val)?; + } + "corner-smoothing" if n0_xml => { + if corner_smoothing_seen { + return err(format!( + "duplicate corner-smoothing attribute on <{tag}>" + )); + } + corner_smoothing_seen = true; + let value = parse_num(&val, "corner-smoothing")?; + if !(0.0..=1.0).contains(&value) { + return err("corner-smoothing must be between 0 and 1 inclusive"); + } + corner_smoothing = CornerSmoothing(value); + } + "rotation" => header.rotation = parse_num(&val, "rotation")?, + "flip-x" => header.flip_x = parse_bool(&val, "flip-x", strict)?, + "flip-y" => header.flip_y = parse_bool(&val, "flip-y", strict)?, + "flow" => { + if is_authored_root { + return err("the authored root cannot declare `flow`"); + } + flow_seen = true; + header.flow = match val.as_str() { + "absolute" => Flow::Absolute, + "in" => Flow::InFlow, + _ => return err(format!("bad flow `{val}`")), + } + } + "grow" => { + if is_authored_root { + return err("the authored root cannot declare `grow`"); + } + grow_seen = true; + header.grow = parse_non_negative(&val, "grow", strict)?; + } + "align" => { + if is_authored_root { + return err("the authored root cannot declare `align`"); + } + align_seen = true; + header.self_align = match val.as_str() { + "start" => SelfAlign::Start, + "center" => SelfAlign::Center, + "end" => SelfAlign::End, + "stretch" => SelfAlign::Stretch, + _ => return err(format!("bad align `{val}`")), + } + } + "opacity" => { + let opacity = parse_num(&val, "opacity")?; + if strict && !(0.0..=1.0).contains(&opacity) { + return err("opacity must be between 0 and 1 inclusive"); + } + header.opacity = opacity; + } + "hidden" => header.active = !parse_bool(&val, "hidden", strict)?, + "layout" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + layout.mode = match val.as_str() { + "flex" => LayoutMode::Flex, + "none" => LayoutMode::None, + _ => return err(format!("bad layout `{val}`")), + } + } + "direction" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + flex_only_attr.get_or_insert_with(|| key.clone()); + layout.direction = match val.as_str() { + "row" => Direction::Row, + "column" => Direction::Column, + _ => return err(format!("bad direction `{val}`")), + } + } + "wrap" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + flex_only_attr.get_or_insert_with(|| key.clone()); + layout.wrap = parse_bool(&val, "wrap", strict)?; + } + "main" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + flex_only_attr.get_or_insert_with(|| key.clone()); + layout.main_align = match val.as_str() { + "start" => MainAlign::Start, + "center" => MainAlign::Center, + "end" => MainAlign::End, + "space-between" => MainAlign::SpaceBetween, + "space-around" => MainAlign::SpaceAround, + "space-evenly" => MainAlign::SpaceEvenly, + _ => return err(format!("bad main `{val}`")), + } + } + "cross" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + flex_only_attr.get_or_insert_with(|| key.clone()); + layout.cross_align = match val.as_str() { + "start" => CrossAlign::Start, + "center" => CrossAlign::Center, + "end" => CrossAlign::End, + "stretch" => CrossAlign::Stretch, + _ => return err(format!("bad cross `{val}`")), + } + } + "gap" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + flex_only_attr.get_or_insert_with(|| key.clone()); + let parts: Vec<&str> = val.split_whitespace().collect(); + match parts.as_slice() { + [g] => { + layout.gap_main = parse_non_negative(g, "gap", strict)?; + layout.gap_cross = layout.gap_main; + } + [m, c] => { + layout.gap_main = parse_non_negative(m, "gap", strict)?; + layout.gap_cross = parse_non_negative(c, "gap", strict)?; + } + _ => return err("gap takes 1 or 2 numbers"), + } + } + "padding" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + let nums: Vec = val + .split_whitespace() + .map(|p| parse_non_negative(p, "padding", strict)) + .collect::>()?; + layout.padding = match nums.as_slice() { + [a] => EdgeInsets::all(*a), + [t, r, b, l] => EdgeInsets { + top: *t, + right: *r, + bottom: *b, + left: *l, + }, + _ => return err("padding takes 1 or 4 numbers"), + }; + } + "clips" => { + frame_only_attr.get_or_insert_with(|| key.clone()); + clips = parse_bool(&val, "clips", strict)?; + } + "kind" if !n0_xml => { + shape_only_attr.get_or_insert_with(|| key.clone()); + shape_kind = Some(match val.as_str() { + "rect" => ShapeDesc::Rect, + "ellipse" => ShapeDesc::Ellipse, + "line" => ShapeDesc::Line, + _ => return err(format!("bad shape kind `{val}`")), + }) + } + "size" if !n0_xml => { + text_only_attr.get_or_insert_with(|| key.clone()); + font_size = parse_positive(&val, "size", false)?; + } + "font-size" if n0_xml => { + text_only_attr.get_or_insert_with(|| key.clone()); + font_size = parse_positive(&val, "font-size", true)?; + } + "size" if n0_xml => return err( + "text attribute `size` belongs to historical textir; use `font-size`", + ), + "font-weight" if n0_xml => { + text_only_attr.get_or_insert_with(|| key.clone()); + font_weight = parse_font_weight(&val, "text")?; + } + "font-style" if n0_xml => { + text_only_attr.get_or_insert_with(|| key.clone()); + font_style_italic = parse_font_style(&val, "text")?; + } + "ops" => { + lens_only_attr.get_or_insert_with(|| key.clone()); + lens_ops = parse_lens_ops(&val, strict)?; + } + "d" if n0_xml => { + path_only_attr.get_or_insert_with(|| key.clone()); + path_d = Some(val); + } + "fill-rule" if n0_xml => { + path_only_attr.get_or_insert_with(|| key.clone()); + path_fill_rule = match val.as_str() { + "nonzero" => FillRule::NonZero, + "evenodd" => FillRule::EvenOdd, + _ => { + return err(format!( + "fill-rule on must be `nonzero` or `evenodd`, found `{val}`" + )) + } + }; + } + "fill" => { + let fillable = matches!(node_tag, "frame" | "text") + || node_tag == "path" + || matches!( + shape_kind.as_ref(), + Some(ShapeDesc::Rect | ShapeDesc::Ellipse) + ); + if strict && !fillable { + return err(format!("fill is not valid on <{tag}>")); + } + if legacy_fill.is_some() { + return err(format!("duplicate `fill` on <{tag}>")); + } + if strict { + legacy_fill = Some(parse_color(&val, "fill")?); + } else { + legacy_fill = Some(val.into()); + } + } + _ => return err(format!("unknown attribute `{key}` on <{tag}>")), + } + } + + if n0_xml { + let parent_is_flex = stack.last().is_some_and(|parent| { + matches!( + &nodes.get(&parent.id).expect("open parent exists").payload, + Payload::Frame { layout, .. } if layout.mode == LayoutMode::Flex + ) + }); + if flow_seen && !parent_is_flex { + return err("flow is only valid on a child of a flex container"); + } + if parent_is_flex && header.flow == Flow::InFlow && (x_seen || y_seen) { + return err("x/y are not valid on an in-flow child of a flex container"); + } + if (!parent_is_flex || header.flow != Flow::InFlow) && (grow_seen || align_seen) + { + return err( + "grow/align are only valid on an in-flow child of a flex container", + ); + } + if node_tag != "frame" { + if let Some(attr) = frame_only_attr { + return err(format!("attribute `{attr}` is only valid on ")); + } + } + if node_tag != "shape" { + if let Some(attr) = shape_only_attr { + return err(format!("attribute `{attr}` is only valid on ")); + } + } + if node_tag != "text" { + if let Some(attr) = text_only_attr { + return err(format!("attribute `{attr}` is only valid on ")); + } + } + if node_tag != "lens" { + if let Some(attr) = lens_only_attr { + return err(format!("attribute `{attr}` is only valid on ")); + } + } + if node_tag != "path" { + if let Some(attr) = path_only_attr { + return err(format!("attribute `{attr}` is only valid on ")); + } + } + if layout.mode != LayoutMode::Flex { + if let Some(attr) = flex_only_attr { + return err(format!("attribute `{attr}` requires layout=\"flex\"")); + } + } + if matches!(node_tag, "group" | "lens") + && (width_seen || height_seen || box_constraint_seen) + { + return err(format!( + "<{tag}> has a derived box and cannot declare size constraints" + )); + } + if matches!(node_tag, "group" | "lens") + && (matches!(header.x, AxisBinding::Span { .. }) + || matches!(header.y, AxisBinding::Span { .. })) + { + return err(format!( + "<{tag}> has a derived origin and cannot use Span bindings" + )); + } + if aspect_seen && !matches!(node_tag, "shape" | "path") { + return err( + "aspect-ratio is only valid on and , or on ", + ); + } + let supports_corners = + node_tag == "frame" || matches!(shape_kind.as_ref(), Some(ShapeDesc::Rect)); + if (corner_radius_seen || corner_smoothing_seen) && !supports_corners { + return err(format!( + "corner-radius and corner-smoothing are only valid on and , not <{tag}>" + )); + } + if let Err(reason) = + renderability::validate_smooth_corner_radii(corner_radius, corner_smoothing) + { + return err(reason.to_string()); + } + if matches!(header.x, AxisBinding::Span { .. }) + && (width_seen || header.min_width.is_some() || header.max_width.is_some()) + { + return err( + "a span x binding cannot also declare width/min-width/max-width", + ); + } + if matches!(header.y, AxisBinding::Span { .. }) + && (height_seen + || header.min_height.is_some() + || header.max_height.is_some()) + { + return err( + "a span y binding cannot also declare height/min-height/max-height", + ); + } + if matches!(node_tag, "shape" | "path") { + match shape_kind.as_ref() { + Some(ShapeDesc::Rect | ShapeDesc::Ellipse) => { + let width_supplied = matches!( + (header.width, header.x), + (SizeIntent::Fixed(_), _) | (_, AxisBinding::Span { .. }) + ); + let height_supplied = matches!( + (header.height, header.y), + (SizeIntent::Fixed(_), _) | (_, AxisBinding::Span { .. }) + ); + let valid = matches!( + (width_supplied, height_supplied, aspect_seen), + (true, true, false) | (true, false, true) | (false, true, true) + ); + if !valid { + return err( + " and require both axes supplied by a fixed size or Span, or exactly one supplied axis plus aspect-ratio", + ); + } + } + Some(ShapeDesc::Line) => { + let width_supplied = matches!( + (header.width, header.x), + (SizeIntent::Fixed(_), _) | (_, AxisBinding::Span { .. }) + ); + if !width_supplied { + return err(" requires a fixed width or x Span"); + } + if matches!(header.y, AxisBinding::Span { .. }) { + return err(" must not declare a y Span"); + } + if height_seen { + return err(" must not declare height"); + } + if header.min_height.is_some() || header.max_height.is_some() { + return err(" must not declare min-height/max-height"); + } + if header.aspect_ratio.is_some() { + return err(" must not declare aspect-ratio"); + } + } + Some(ShapeDesc::Path(_)) => unreachable!("path is analyzed below"), + None if node_tag == "path" => { + let width_supplied = matches!( + (header.width, header.x), + (SizeIntent::Fixed(_), _) | (_, AxisBinding::Span { .. }) + ); + let height_supplied = matches!( + (header.height, header.y), + (SizeIntent::Fixed(_), _) | (_, AxisBinding::Span { .. }) + ); + let valid = matches!( + (width_supplied, height_supplied, aspect_seen), + (true, true, false) | (true, false, true) | (false, true, true) + ); + if !valid { + return err( + " requires both axes supplied by a fixed size or Span, or exactly one supplied axis plus aspect-ratio", + ); + } + } + None => {} + } + } + } + + let payload = match node_tag { + "frame" => { + // Kind defaults (§4): frame sizes are Fixed-required; + // the IR treats missing w/h as auto (hug) which is + // legal for frames. + Payload::Frame { + layout, + clips_content: clips, + } + } + "shape" => { + let desc = shape_kind + .take() + .ok_or_else(|| ParseError(" requires kind".into()))?; + if matches!(desc, ShapeDesc::Line) { + header.height = SizeIntent::Fixed(0.0); // §3.2 locked + } + Payload::Shape { desc } + } + "path" => { + let d = path_d.ok_or_else(|| ParseError(" requires `d`".into()))?; + let artifact = path::analyze(d, path_fill_rule) + .map_err(|error| ParseError(format!(" d: {error}")))?; + Payload::Shape { + desc: ShapeDesc::Path(artifact), + } + } + "text" => Payload::Text { + content: String::new(), // filled at End + font_size, + }, + "group" => Payload::Group, + "lens" => Payload::Lens { ops: lens_ops }, + _ => return err(format!("unknown element <{tag}>")), + }; + renderability::validate_geometry(&header, &payload) + .map_err(|error| ParseError(error.to_string()))?; + + let id = next_id; + next_id += 1; + let legacy_fill_seen = legacy_fill.is_some(); + let fills = match legacy_fill { + Some(color) => Paints::solid(color), + None if n0_xml => n0_xml_default_fills(&payload), + None => Paints::default(), + }; + let mut node = Node::new(id, header, payload); + node.corner_radius = corner_radius; + node.corner_smoothing = corner_smoothing; + node.fills = fills; + nodes.insert(id, node); + if let Some(parent) = stack.last() { + let pid = parent.id; + nodes.get_mut(&pid).unwrap().children.push(id); + } else if n0_xml { + if render_root.is_some() { + return err("multiple render roots in "); + } + let root_id = root.expect("grida.xml root initialized"); + nodes.get_mut(&root_id).unwrap().children.push(id); + render_root = Some(id); + } else if root.is_none() { + root = Some(id); + } else { + return err("multiple root elements"); + } + + let is_text = node_tag == "text"; + stack.push(Pending { + id, + tag, + text_content: String::new(), + text_segments: vec![], + default_text_style: is_text.then_some(TextStyleRec { + font_size, + font_weight, + font_style_italic, + }), + is_text, + fill_seen: false, + stroke_seen: false, + legacy_fill_seen, + content_started: false, + }); + // Self-closing (`Event::Empty`) has no matching End event. + if is_empty { + finish(&mut stack, &mut nodes, None)?; + } + } + Ok(Event::Text(t)) => { + let txt = t.unescape().map_err(|e| ParseError(format!("text: {e}")))?; + if n0_xml && !declaration_seen && !txt.is_empty() { + pre_declaration_content = true; + } + if let Some(p) = stack.last_mut() { + if n0_xml && !p.is_text && !txt.trim().is_empty() { + return err(format!("character content is not allowed in <{}>", p.tag)); + } + if p.is_text && !txt.trim().is_empty() { + p.content_started = true; + } + p.text_content.push_str(&txt); + } else if n0_xml && !txt.trim().is_empty() { + return err("character content is not allowed outside the document envelope"); + } + } + Ok(Event::End(e)) => { + let tag = String::from_utf8_lossy(e.name().as_ref()).to_string(); + if n0_xml && tag == "grida" { + if !envelope_open || envelope_closed { + return err("unbalanced "); + } + if !stack.is_empty() { + return err(" closed before its render root"); + } + if render_root.is_none() { + return err(" must contain exactly one render root"); + } + envelope_open = false; + envelope_closed = true; + continue; + } + finish(&mut stack, &mut nodes, Some(&tag))?; + } + Ok(Event::Decl(decl)) if n0_xml => { + if declaration_seen { + return err("duplicate XML declaration"); + } + if pre_declaration_content || envelope_open || envelope_closed { + return err("XML declaration must be the first document event"); + } + validate_n0_xml_declaration(&decl)?; + declaration_seen = true; + } + Ok(Event::Comment(_)) if n0_xml => { + if !declaration_seen { + pre_declaration_content = true; + } + } + Ok(Event::CData(_)) if n0_xml => { + return err("CDATA is not supported; use escaped text content"); + } + Ok(Event::PI(_) | Event::DocType(_)) if n0_xml => { + return err("processing instructions and doctypes are not supported"); + } + Ok(_) => {} + } + } + + if !stack.is_empty() { + return err("unclosed elements"); + } + if n0_xml { + if envelope_open { + return err("unclosed envelope"); + } + if !envelope_closed { + return err("missing envelope"); + } + if render_root.is_none() { + return err(" must contain exactly one render root"); + } + } + let root = root.ok_or_else(|| ParseError("empty document".into()))?; + // The scene root spans the viewport unless bindings were given. + Ok(Document::from_map(nodes, root)) +} + +fn finish( + stack: &mut Vec, + nodes: &mut BTreeMap, + end_tag: Option<&str>, +) -> Result<(), ParseError> { + if let Some(end_tag) = end_tag { + let start_tag = stack + .last() + .ok_or_else(|| ParseError("unbalanced end".into()))? + .tag + .as_str(); + if start_tag != end_tag { + return err(format!("mismatched end tag for <{start_tag}>")); + } + } + let mut p = stack + .pop() + .ok_or_else(|| ParseError("unbalanced end".into()))?; + if p.is_text { + let default_style = p + .default_text_style + .expect("text pending state carries its default style"); + if !p.text_content.is_empty() { + p.text_segments.push(PendingTextSegment { + text: p.text_content, + style: default_style, + fills: None, + }); + } + + let mut text = String::new(); + let mut runs = Vec::new(); + for segment in p.text_segments { + if segment.text.is_empty() { + continue; + } + let start = u32::try_from(text.len()) + .map_err(|_| ParseError("attributed text exceeds u32 byte offsets".into()))?; + text.push_str(&segment.text); + let end = u32::try_from(text.len()) + .map_err(|_| ParseError("attributed text exceeds u32 byte offsets".into()))?; + runs.push(StyledTextRun { + start, + end, + style: segment.style, + fills: segment.fills, + }); + } + let mut attributed = if text.is_empty() { + AttributedString::new(text, default_style) + } else { + AttributedString::from_runs(text, runs).map_err(ParseError)? + }; + attributed.merge_adjacent_runs(); + + let can_use_uniform_payload = default_style.font_weight + == TextStyleRec::DEFAULT_FONT_WEIGHT + && !default_style.font_style_italic + && attributed.is_uniform_default(default_style); + nodes.get_mut(&p.id).unwrap().payload = if can_use_uniform_payload { + Payload::Text { + content: attributed.text, + font_size: default_style.font_size, + } + } else { + Payload::AttributedText { + attributed_string: attributed, + default_style, + } + }; + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Canonical printer — shortest honest form, defaults omitted +// --------------------------------------------------------------------------- + +fn fmt_num(v: f32) -> String { + if v == v.trunc() && v.abs() < 1e7 { + format!("{}", v as i64) + } else { + format!("{v}") + } +} + +fn fmt_corner_axis(values: [f32; 4]) -> String { + if values[1..].iter().all(|value| *value == values[0]) { + fmt_num(values[0]) + } else { + values.map(fmt_num).join(" ") + } +} + +fn fmt_corner_radius(radius: RectangularCornerRadius) -> String { + let rx = [radius.tl.rx, radius.tr.rx, radius.br.rx, radius.bl.rx]; + let ry = [radius.tl.ry, radius.tr.ry, radius.br.ry, radius.bl.ry]; + let rx_text = fmt_corner_axis(rx); + if rx == ry { + rx_text + } else { + format!("{rx_text} / {}", fmt_corner_axis(ry)) + } +} + +fn fmt_num_f64(v: f64) -> String { + format!("{v}") +} + +fn fmt_binding(b: AxisBinding) -> Option { + match b { + AxisBinding::Pin { + anchor: AnchorEdge::Start, + offset, + } => { + if offset == 0.0 { + None // default + } else { + Some(fmt_num(offset)) + } + } + AxisBinding::Pin { + anchor: AnchorEdge::End, + offset, + } => Some(format!("end {}", fmt_num(offset))), + AxisBinding::Pin { + anchor: AnchorEdge::Center, + offset, + } => Some(if offset == 0.0 { + "center".to_string() + } else { + format!("center {}", fmt_num(offset)) + }), + AxisBinding::Span { start, end } => { + Some(format!("span {} {}", fmt_num(start), fmt_num(end))) + } + } +} + +fn push_attr(out: &mut String, key: &str, val: &str) { + let escaped = val + .replace('&', "&") + .replace('"', """) + .replace('<', "<") + .replace('>', ">"); + let _ = write!(out, " {key}=\"{escaped}\""); +} + +fn escape_text(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +fn validate_text_style_for_write(style: TextStyleRec, target: &str) -> Result<(), String> { + if !style.font_size.is_finite() || style.font_size <= 0.0 { + return Err(format!( + "font-size on <{target}> must be finite and greater than zero" + )); + } + if !(1..=1000).contains(&style.font_weight) { + return Err(format!( + "font-weight on <{target}> must be an integer from 1 through 1000" + )); + } + Ok(()) +} + +fn push_text_style_overrides(out: &mut String, style: TextStyleRec, inherited: TextStyleRec) { + if style.font_size != inherited.font_size { + push_attr(out, "font-size", &fmt_num(style.font_size)); + } + if style.font_weight != inherited.font_weight { + push_attr(out, "font-weight", &style.font_weight.to_string()); + } + if style.font_style_italic != inherited.font_style_italic { + push_attr( + out, + "font-style", + if style.font_style_italic { + "italic" + } else { + "normal" + }, + ); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrintError(pub String); + +impl std::fmt::Display for PrintError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "textir: {}", self.0) + } +} + +impl std::error::Error for PrintError {} + +/// The historical TextIr dialect has only a singleton opaque solid `fill` attribute. +/// This fallible entry point refuses richer model state instead of narrowing +/// it. New file-first documents should use [`crate::n0_xml::print`]. +pub fn try_print(doc: &Document) -> Result { + let mut out = String::new(); + print_node(doc, doc.root, 0, Dialect::TextIr, &mut out).map_err(PrintError)?; + Ok(out) +} + +pub fn print(doc: &Document) -> String { + try_print(doc).expect("document is not representable by the historical TextIr dialect") +} + +pub(crate) fn print_n0_xml_render_root( + doc: &Document, + id: NodeId, + depth: usize, + out: &mut String, +) -> Result<(), String> { + print_node(doc, id, depth, Dialect::N0Xml, out) +} + +#[derive(Debug, Clone, Copy)] +enum FillEmission<'a> { + Omit, + Attribute(Color), + Empty, + Stack(&'a Paints), +} + +fn n0_xml_fill_emission(node: &Node) -> Result, String> { + if !supports_fill(&node.payload) && !node.fills.is_empty() { + return Err(format!( + "<{}> cannot carry fills", + path_aware_kind_name(&node.payload) + )); + } + let default = n0_xml_default_fills(&node.payload); + if node.fills == default { + Ok(FillEmission::Omit) + } else if let [Paint::Solid(solid)] = node.fills.as_slice() { + if solid.active && solid.color.alpha() == 255 && solid.blend_mode == BlendMode::Normal { + Ok(FillEmission::Attribute(solid.color)) + } else { + Ok(FillEmission::Stack(&node.fills)) + } + } else if node.fills.is_empty() { + Ok(FillEmission::Empty) + } else { + Ok(FillEmission::Stack(&node.fills)) + } +} + +fn n0_xml_run_fill_emission(fills: &Paints) -> FillEmission<'_> { + if let [Paint::Solid(solid)] = fills.as_slice() { + if solid.active && solid.color.alpha() == 255 && solid.blend_mode == BlendMode::Normal { + FillEmission::Attribute(solid.color) + } else { + FillEmission::Stack(fills) + } + } else if fills.is_empty() { + FillEmission::Empty + } else { + FillEmission::Stack(fills) + } +} + +fn historical_fill(node: &Node) -> Result, String> { + if node + .strokes + .iter() + .any(|stroke| !(stroke.paints.is_empty() && stroke.geometry_is_default_for(&node.payload))) + { + return Err(format!( + "node {} has strokes the historical TextIr dialect cannot represent", + node.id + )); + } + match node.fills.as_slice() { + [] => Ok(None), + [Paint::Solid(solid)] + if solid.active + && solid.blend_mode == BlendMode::Normal + && solid.color.alpha() == 255 => + { + Ok(Some(solid.color)) + } + _ => Err(format!( + "node {} has a paint stack the historical TextIr dialect cannot represent", + node.id + )), + } +} + +fn validate_corner_style_for_write(node: &Node, n0_xml: bool) -> Result<(), String> { + let radii = [ + node.corner_radius.tl, + node.corner_radius.tr, + node.corner_radius.br, + node.corner_radius.bl, + ]; + if radii + .iter() + .any(|radius| !radius.rx.is_finite() || !radius.ry.is_finite()) + { + return Err(format!("node {} has non-finite corner radii", node.id)); + } + if radii + .iter() + .any(|radius| radius.rx < 0.0 || radius.ry < 0.0) + { + return Err(format!("node {} has negative corner radii", node.id)); + } + let smoothing = node.corner_smoothing.value(); + if !smoothing.is_finite() || !(0.0..=1.0).contains(&smoothing) { + return Err(format!( + "node {} corner-smoothing must be finite and between 0 and 1", + node.id + )); + } + + let has_corner_style = !node.corner_radius.is_zero() || !node.corner_smoothing.is_zero(); + if !has_corner_style { + return Ok(()); + } + if !n0_xml { + return Err(format!( + "node {} has corner geometry the historical TextIr dialect cannot represent", + node.id + )); + } + let supports_corners = matches!(node.payload, Payload::Frame { .. }) + || matches!( + node.payload, + Payload::Shape { + desc: ShapeDesc::Rect + } + ); + if !supports_corners { + return Err(format!( + "<{}> cannot carry corner-radius or corner-smoothing", + path_aware_kind_name(&node.payload) + )); + } + if renderability::validate_smooth_corner_radii(node.corner_radius, node.corner_smoothing) + .is_err() + { + return Err(format!( + "node {} uses nonzero corner-smoothing with elliptical radii; Draft 0 cannot render that state losslessly", + node.id + )); + } + Ok(()) +} + +fn validate_path_box_for_write(node: &Node) -> Result<(), String> { + if !matches!( + &node.payload, + Payload::Shape { + desc: ShapeDesc::Path(_) + } + ) { + return Ok(()); + } + + let validate_size = |intent: SizeIntent, axis: &str| match intent { + SizeIntent::Auto => Ok(()), + SizeIntent::Fixed(value) if value.is_finite() && value >= 0.0 => Ok(()), + SizeIntent::Fixed(_) => Err(format!( + " {axis} must be a finite non-negative number" + )), + }; + validate_size(node.header.width, "width")?; + validate_size(node.header.height, "height")?; + + let validate_binding = |binding: AxisBinding, axis: &str| { + let finite = match binding { + AxisBinding::Pin { offset, .. } => offset.is_finite(), + AxisBinding::Span { start, end } => start.is_finite() && end.is_finite(), + }; + if finite { + Ok(()) + } else { + Err(format!(" {axis} binding must contain finite numbers")) + } + }; + validate_binding(node.header.x, "x")?; + validate_binding(node.header.y, "y")?; + + for (name, value) in [ + ("min-width", node.header.min_width), + ("max-width", node.header.max_width), + ("min-height", node.header.min_height), + ("max-height", node.header.max_height), + ] { + if value.is_some_and(|value| !value.is_finite() || value < 0.0) { + return Err(format!( + " {name} must be a finite non-negative number" + )); + } + } + + if matches!(node.header.x, AxisBinding::Span { .. }) + && (matches!(node.header.width, SizeIntent::Fixed(_)) + || node.header.min_width.is_some() + || node.header.max_width.is_some()) + { + return Err("a span x binding cannot also declare width/min-width/max-width".into()); + } + if matches!(node.header.y, AxisBinding::Span { .. }) + && (matches!(node.header.height, SizeIntent::Fixed(_)) + || node.header.min_height.is_some() + || node.header.max_height.is_some()) + { + return Err("a span y binding cannot also declare height/min-height/max-height".into()); + } + + let has_aspect = match node.header.aspect_ratio { + None => false, + Some((a, b)) if a.is_finite() && b.is_finite() && a > 0.0 && b > 0.0 => true, + Some(_) => { + return Err(" aspect-ratio terms must be finite and greater than zero".into()) + } + }; + let width_supplied = matches!(node.header.width, SizeIntent::Fixed(_)) + || matches!(node.header.x, AxisBinding::Span { .. }); + let height_supplied = matches!(node.header.height, SizeIntent::Fixed(_)) + || matches!(node.header.y, AxisBinding::Span { .. }); + if !matches!( + (width_supplied, height_supplied, has_aspect), + (true, true, false) | (true, false, true) | (false, true, true) + ) { + return Err( + " requires both axes supplied by a fixed size or Span, or exactly one supplied axis plus aspect-ratio" + .into(), + ); + } + Ok(()) +} + +pub(crate) fn validate_path_for_write(node: &Node) -> Result<(), String> { + let Payload::Shape { + desc: ShapeDesc::Path(artifact), + } = &node.payload + else { + return Ok(()); + }; + validate_path_box_for_write(node)?; + artifact + .validate() + .map_err(|error| format!("node {} has invalid path data: {error}", node.id))?; + if artifact.source_reference_box() != (1.0, 1.0) { + return Err(format!( + "node {} path d is not authored in Grida's unit reference box", + node.id + )); + } + let validated = path::analyze(artifact.d(), artifact.fill_rule()) + .map_err(|error| format!("node {} has invalid path data: {error}", node.id))?; + // Reanalysis starts from the same authored d and must reproduce every + // derived field consumed by bounds, damage, and paint. + if validated.geometry() != artifact.geometry() { + return Err(format!( + "node {} has a path artifact inconsistent with its authored d", + node.id + )); + } + Ok(()) +} + +fn push_common_paint_attrs( + out: &mut String, + active: bool, + opacity: f32, + blend_mode: BlendMode, + _tag: &str, +) -> Result<(), String> { + if !active { + push_attr(out, "visible", "false"); + } + if opacity != 1.0 { + push_attr(out, "opacity", &fmt_num(opacity)); + } + if blend_mode != BlendMode::Normal { + push_attr(out, "blend-mode", blend_mode.as_str()); + } + Ok(()) +} + +fn push_transform_attr( + out: &mut String, + transform: crate::math::Affine, + _tag: &str, +) -> Result<(), String> { + if transform != crate::math::Affine::IDENTITY { + push_attr( + out, + "transform", + &format!( + "{} {} {} {} {} {}", + fmt_num(transform.a), + fmt_num(transform.b), + fmt_num(transform.c), + fmt_num(transform.d), + fmt_num(transform.e), + fmt_num(transform.f) + ), + ); + } + Ok(()) +} + +fn write_gradient_stops( + stops: &[GradientStop], + _tag: &str, + depth: usize, + out: &mut String, +) -> Result<(), String> { + let indent = " ".repeat(depth); + for stop in stops { + let _ = write!(out, "{indent}"); + } + Ok(()) +} + +fn write_gradient(paint: &Paint, depth: usize, out: &mut String) -> Result<(), String> { + let (kind, transform, stops, tile_mode, endpoints) = match paint { + Paint::LinearGradient(gradient) => ( + "linear", + gradient.transform, + gradient.stops.as_slice(), + Some(gradient.tile_mode), + Some((gradient.xy1, gradient.xy2)), + ), + Paint::RadialGradient(gradient) => ( + "radial", + gradient.transform, + gradient.stops.as_slice(), + Some(gradient.tile_mode), + None, + ), + Paint::SweepGradient(gradient) => ( + "sweep", + gradient.transform, + gradient.stops.as_slice(), + None, + None, + ), + Paint::DiamondGradient(gradient) => ( + "diamond", + gradient.transform, + gradient.stops.as_slice(), + None, + None, + ), + Paint::Solid(_) | Paint::Image(_) => unreachable!("gradient writer requires a gradient"), + }; + let endpoints = match endpoints { + Some((from, to)) => { + if !from.0.is_finite() || !from.1.is_finite() || !to.0.is_finite() || !to.1.is_finite() + { + return Err(" endpoints must be finite".into()); + } + if from == to { + return Err(" from and to must differ".into()); + } + Some(( + from.try_to_uv().ok_or_else(|| { + " from alignment is not representable: binary64 UV arithmetic cannot reproduce its stored f32 components".to_string() + })?, + to.try_to_uv().ok_or_else(|| { + " to alignment is not representable: binary64 UV arithmetic cannot reproduce its stored f32 components".to_string() + })?, + from != Alignment::CENTER_LEFT || to != Alignment::CENTER_RIGHT, + )) + } + None => None, + }; + + let indent = " ".repeat(depth); + let _ = write!(out, "{indent}"); + write_gradient_stops(stops, "gradient", depth + 1, out)?; + let _ = writeln!(out, "{indent}"); + Ok(()) +} + +fn write_paint(paint: &Paint, depth: usize, out: &mut String) -> Result<(), String> { + renderability::validate_paint(paint).map_err(|error| error.to_string())?; + let indent = " ".repeat(depth); + match paint { + Paint::Solid(solid) => { + let _ = write!(out, "{indent}"); + } + paint @ (Paint::LinearGradient(_) + | Paint::RadialGradient(_) + | Paint::SweepGradient(_) + | Paint::DiamondGradient(_)) => { + write_gradient(paint, depth, out)?; + } + Paint::Image(image) => { + let src = match &image.image { + ResourceRef::Rid(src) if !src.trim().is_empty() => src, + ResourceRef::Rid(_) => return Err(" src must not be empty".into()), + ResourceRef::Hash(_) => { + return Err("Draft 0 XML cannot represent a hash image resource".into()); + } + }; + let fit = match image.fit { + ImagePaintFit::Fit(fit) => fit, + ImagePaintFit::Transform(_) | ImagePaintFit::Tile(_) => { + unreachable!("render capability validation accepts only named image fits") + } + }; + let _ = write!(out, "{indent}"); + } + } + Ok(()) +} + +fn write_fill( + emission: FillEmission<'_>, + depth: usize, + inline: bool, + out: &mut String, +) -> Result<(), String> { + match emission { + FillEmission::Omit | FillEmission::Attribute(_) => {} + FillEmission::Empty => { + if !inline { + let _ = write!(out, "{}", " ".repeat(depth)); + } + if inline { + let _ = write!(out, ""); + } else { + let _ = writeln!(out, ""); + } + } + FillEmission::Stack(paints) => { + if !inline { + let _ = write!(out, "{}", " ".repeat(depth)); + } + let _ = writeln!(out, ""); + for paint in paints.iter() { + write_paint(paint, depth + 1, out)?; + } + if inline { + let _ = write!(out, "{}", " ".repeat(depth)); + } else { + let _ = writeln!(out, "{}", " ".repeat(depth)); + } + } + } + Ok(()) +} + +fn write_attributed_text_content( + attributed: &AttributedString, + default_style: TextStyleRec, + depth: usize, + out: &mut String, +) -> Result<(), String> { + attributed.validate()?; + let mut attributed = attributed.clone(); + attributed.merge_adjacent_runs(); + if attributed.text.is_empty() && !attributed.is_uniform_default(default_style) { + return Err( + "empty attributed text cannot preserve a styled or painted zero-length run".into(), + ); + } + + for run in &attributed.runs { + validate_text_style_for_write(run.style, "tspan")?; + let run_text = escape_text(attributed.run_text(run)); + if run.style == default_style && run.fills.is_none() { + let _ = write!(out, "{run_text}"); + continue; + } + + let _ = write!(out, ""); + if let Some(emission @ (FillEmission::Empty | FillEmission::Stack(_))) = fill_emission { + write_fill(emission, depth + 1, true, out)?; + } + let _ = write!(out, "{run_text}"); + } + Ok(()) +} + +fn normalized_dash_array(values: &[f32]) -> Vec { + if values.len().is_multiple_of(2) { + values.to_vec() + } else { + values.iter().chain(values).copied().collect() + } +} + +fn stroke_is_omitted(stroke: &Stroke, payload: &Payload) -> bool { + stroke.paints.is_empty() && stroke.geometry_is_default_for(payload) +} + +fn write_stroke( + stroke: &Stroke, + payload: &Payload, + corner_smoothing: CornerSmoothing, + depth: usize, + inline: bool, + out: &mut String, +) -> Result<(), String> { + renderability::validate_stroke(stroke, payload, corner_smoothing) + .map_err(|error| error.to_string())?; + if stroke_is_omitted(stroke, payload) { + return Ok(()); + } + let default = Stroke::default_for(payload).expect("validated stroke target"); + if !inline { + let _ = write!(out, "{}", " ".repeat(depth)); + } + let _ = write!(out, " fmt_num(0.0), + StrokeWidth::Uniform(width) => fmt_num(width), + StrokeWidth::Rectangular(widths) => widths.values().map(fmt_num).join(" "), + }; + push_attr(out, "width", &value); + } + if stroke.align != default.align { + push_attr(out, "align", stroke.align.as_str()); + } + if matches!( + payload, + Payload::Shape { + desc: ShapeDesc::Line + } | Payload::Shape { + desc: ShapeDesc::Path(_) + } + ) && stroke.cap != default.cap + { + push_attr(out, "cap", stroke.cap.as_str()); + } + if matches!( + payload, + Payload::Frame { .. } + | Payload::Shape { + desc: ShapeDesc::Rect + } + | Payload::Shape { + desc: ShapeDesc::Path(_) + } + ) { + if stroke.join != default.join { + push_attr(out, "join", stroke.join.as_str()); + } + if stroke.miter_limit != default.miter_limit { + push_attr(out, "miter-limit", &fmt_num(stroke.miter_limit)); + } + } + if let Some(values) = &stroke.dash_array { + let value = normalized_dash_array(values) + .iter() + .map(|value| fmt_num(*value)) + .collect::>() + .join(" "); + push_attr(out, "dash-array", &value); + } + if stroke.paints.is_empty() { + if inline { + let _ = write!(out, "/>"); + } else { + let _ = writeln!(out, "/>"); + } + return Ok(()); + } + let _ = writeln!(out, ">"); + for paint in stroke.paints.iter() { + write_paint(paint, depth + 1, out)?; + } + if inline { + let _ = write!(out, "{}", " ".repeat(depth)); + } else { + let _ = writeln!(out, "{}", " ".repeat(depth)); + } + Ok(()) +} + +fn print_node( + doc: &Document, + id: NodeId, + depth: usize, + dialect: Dialect, + out: &mut String, +) -> Result<(), String> { + let node = doc.get(id); + let indent = " ".repeat(depth); + let n0_xml = dialect == Dialect::N0Xml; + if node.preserves_descendant_hit_identity() { + let format = if n0_xml { + "n0 XML" + } else { + "the historical TextIr dialect" + }; + return Err(format!( + "node {} is a query-transparent structural wrapper {format} cannot represent", + node.id + )); + } + if !n0_xml + && matches!( + &node.payload, + Payload::Shape { + desc: ShapeDesc::Path(_) + } + ) + { + return Err(format!( + "node {} is a path the historical TextIr dialect cannot represent", + node.id + )); + } + let tag = match (dialect, &node.payload) { + (Dialect::N0Xml, Payload::Frame { .. }) => "container", + ( + Dialect::N0Xml, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ) => "rect", + ( + Dialect::N0Xml, + Payload::Shape { + desc: ShapeDesc::Ellipse, + }, + ) => "ellipse", + ( + Dialect::N0Xml, + Payload::Shape { + desc: ShapeDesc::Line, + }, + ) => "line", + ( + Dialect::N0Xml, + Payload::Shape { + desc: ShapeDesc::Path(_), + }, + ) => "path", + _ => node.payload.kind_name(), + }; + validate_corner_style_for_write(node, n0_xml)?; + renderability::validate_geometry(&node.header, &node.payload) + .map_err(|error| error.to_string())?; + let width_attr = if n0_xml { "width" } else { "w" }; + let height_attr = if n0_xml { "height" } else { "h" }; + let (min_width_attr, max_width_attr, min_height_attr, max_height_attr, aspect_attr) = if n0_xml + { + ( + "min-width", + "max-width", + "min-height", + "max-height", + "aspect-ratio", + ) + } else { + ("min-w", "max-w", "min-h", "max-h", "aspect") + }; + let _ = write!(out, "{indent}<{tag}"); + + if let Some(name) = &node.header.name { + push_attr(out, "name", name); + } + if let Some(b) = fmt_binding(node.header.x) { + push_attr(out, "x", &b); + } + if let Some(b) = fmt_binding(node.header.y) { + push_attr(out, "y", &b); + } + if !node.payload.box_is_derived() { + match node.header.width { + SizeIntent::Fixed(v) => push_attr(out, width_attr, &fmt_num(v)), + SizeIntent::Auto => { + if matches!(node.payload, Payload::Frame { .. }) + && !matches!(node.header.x, AxisBinding::Span { .. }) + { + push_attr(out, width_attr, "auto"); + } + // text: auto is the kind default — omitted + } + } + let is_line = matches!( + node.payload, + Payload::Shape { + desc: ShapeDesc::Line + } + ); + match node.header.height { + SizeIntent::Fixed(v) => { + if !is_line { + push_attr(out, height_attr, &fmt_num(v)); + } + } + SizeIntent::Auto => { + if matches!(node.payload, Payload::Frame { .. }) + && !matches!(node.header.y, AxisBinding::Span { .. }) + { + push_attr(out, height_attr, "auto"); + } + } + } + } + if let Some(v) = node.header.min_width { + push_attr(out, min_width_attr, &fmt_num(v)); + } + if let Some(v) = node.header.max_width { + push_attr(out, max_width_attr, &fmt_num(v)); + } + if let Some(v) = node.header.min_height { + push_attr(out, min_height_attr, &fmt_num(v)); + } + if let Some(v) = node.header.max_height { + push_attr(out, max_height_attr, &fmt_num(v)); + } + if let Some((a, b)) = node.header.aspect_ratio { + push_attr(out, aspect_attr, &format!("{}:{}", fmt_num(a), fmt_num(b))); + } + if n0_xml && !node.corner_radius.is_zero() { + push_attr(out, "corner-radius", &fmt_corner_radius(node.corner_radius)); + } + if n0_xml && !node.corner_smoothing.is_zero() { + push_attr( + out, + "corner-smoothing", + &fmt_num(node.corner_smoothing.value()), + ); + } + if node.header.rotation != 0.0 { + push_attr(out, "rotation", &fmt_num(node.header.rotation)); + } + if node.header.flip_x { + push_attr(out, "flip-x", "true"); + } + if node.header.flip_y { + push_attr(out, "flip-y", "true"); + } + if node.header.flow == Flow::Absolute { + push_attr(out, "flow", "absolute"); + } + if node.header.grow != 0.0 { + push_attr(out, "grow", &fmt_num(node.header.grow)); + } + match node.header.self_align { + SelfAlign::Auto => {} + SelfAlign::Start => push_attr(out, "align", "start"), + SelfAlign::Center => push_attr(out, "align", "center"), + SelfAlign::End => push_attr(out, "align", "end"), + SelfAlign::Stretch => push_attr(out, "align", "stretch"), + } + if node.header.opacity != 1.0 { + push_attr(out, "opacity", &fmt_num(node.header.opacity)); + } + if !node.header.active { + push_attr(out, "hidden", "true"); + } + + match &node.payload { + Payload::Frame { + layout, + clips_content, + } => { + if layout.mode == LayoutMode::Flex { + push_attr(out, "layout", "flex"); + if layout.direction != Direction::Row { + push_attr(out, "direction", "column"); + } + if layout.wrap { + push_attr(out, "wrap", "true"); + } + match layout.main_align { + MainAlign::Start => {} + MainAlign::Center => push_attr(out, "main", "center"), + MainAlign::End => push_attr(out, "main", "end"), + MainAlign::SpaceBetween => push_attr(out, "main", "space-between"), + MainAlign::SpaceAround => push_attr(out, "main", "space-around"), + MainAlign::SpaceEvenly => push_attr(out, "main", "space-evenly"), + } + match layout.cross_align { + CrossAlign::Start => {} + CrossAlign::Center => push_attr(out, "cross", "center"), + CrossAlign::End => push_attr(out, "cross", "end"), + CrossAlign::Stretch => push_attr(out, "cross", "stretch"), + } + if layout.gap_main != 0.0 || layout.gap_cross != 0.0 { + if layout.gap_main == layout.gap_cross { + push_attr(out, "gap", &fmt_num(layout.gap_main)); + } else { + push_attr( + out, + "gap", + &format!("{} {}", fmt_num(layout.gap_main), fmt_num(layout.gap_cross)), + ); + } + } + } + let p = layout.padding; + if p != EdgeInsets::default() { + if p.top == p.right && p.right == p.bottom && p.bottom == p.left { + push_attr(out, "padding", &fmt_num(p.top)); + } else { + push_attr( + out, + "padding", + &format!( + "{} {} {} {}", + fmt_num(p.top), + fmt_num(p.right), + fmt_num(p.bottom), + fmt_num(p.left) + ), + ); + } + } + if *clips_content { + push_attr(out, "clips", "true"); + } + } + Payload::Shape { desc } => { + if let ShapeDesc::Path(artifact) = desc { + debug_assert!(n0_xml, "historical path rejected above"); + push_attr(out, "d", artifact.d()); + if artifact.fill_rule() == FillRule::EvenOdd { + push_attr(out, "fill-rule", "evenodd"); + } + } else if !n0_xml { + let kind = match desc { + ShapeDesc::Rect => "rect", + ShapeDesc::Ellipse => "ellipse", + ShapeDesc::Line => "line", + ShapeDesc::Path(_) => unreachable!(), + }; + push_attr(out, "kind", kind); + } + } + Payload::Text { font_size, .. } => { + if n0_xml { + validate_text_style_for_write(TextStyleRec::from_font_size(*font_size), "text")?; + } + if *font_size != 16.0 { + push_attr( + out, + if n0_xml { "font-size" } else { "size" }, + &fmt_num(*font_size), + ); + } + } + Payload::AttributedText { + attributed_string, + default_style, + } => { + if !n0_xml { + return Err(format!( + "node {} has attributed text the historical TextIr dialect cannot represent", + node.id + )); + } + validate_text_style_for_write(*default_style, "text")?; + attributed_string.validate()?; + push_text_style_overrides(out, *default_style, TextStyleRec::default()); + } + Payload::Group => {} + Payload::Lens { ops } => { + let parts: Vec = ops + .iter() + .map(|op| match op { + LensOp::Translate { x, y } => { + format!("translate({},{})", fmt_num(*x), fmt_num(*y)) + } + LensOp::Rotate { deg } => format!("rotate({})", fmt_num(*deg)), + LensOp::Scale { x, y } => { + if x == y { + format!("scale({})", fmt_num(*x)) + } else { + format!("scale({},{})", fmt_num(*x), fmt_num(*y)) + } + } + LensOp::Skew { x_deg, y_deg } => { + if *y_deg == 0.0 { + format!("skew-x({})", fmt_num(*x_deg)) + } else if *x_deg == 0.0 { + format!("skew-y({})", fmt_num(*y_deg)) + } else { + format!("skew({},{})", fmt_num(*x_deg), fmt_num(*y_deg)) + } + } + LensOp::Matrix { m } => format!( + "matrix({},{},{},{},{},{})", + fmt_num(m[0]), + fmt_num(m[1]), + fmt_num(m[2]), + fmt_num(m[3]), + fmt_num(m[4]), + fmt_num(m[5]) + ), + }) + .collect(); + push_attr(out, "ops", &parts.join(" ")); + } + } + let fill_emission = match dialect { + Dialect::TextIr => { + if let Some(fill) = historical_fill(node)? { + push_attr(out, "fill", &fill.to_hex()); + } + FillEmission::Omit + } + Dialect::N0Xml => n0_xml_fill_emission(node)?, + }; + if let FillEmission::Attribute(color) = fill_emission { + push_attr(out, "fill", &color.to_hex()); + } + let strokes: Vec<&Stroke> = match dialect { + Dialect::TextIr => vec![], + Dialect::N0Xml => { + for stroke in &node.strokes { + renderability::validate_stroke(stroke, &node.payload, node.corner_smoothing) + .map_err(|error| error.to_string())?; + } + node.strokes + .iter() + .filter(|stroke| !stroke_is_omitted(stroke, &node.payload)) + .collect() + } + }; + let has_fill_child = matches!(fill_emission, FillEmission::Empty | FillEmission::Stack(_)); + let has_properties = has_fill_child || !strokes.is_empty(); + + let is_text = node.payload.as_text().is_some(); + if is_text { + let _ = write!(out, ">"); + if has_fill_child { + write_fill(fill_emission, depth + 1, true, out)?; + } + for stroke in strokes { + write_stroke( + stroke, + &node.payload, + node.corner_smoothing, + depth + 1, + true, + out, + )?; + } + match &node.payload { + Payload::Text { content, .. } => { + let _ = write!(out, "{}", escape_text(content)); + } + Payload::AttributedText { + attributed_string, + default_style, + } => write_attributed_text_content(attributed_string, *default_style, depth, out)?, + _ => unreachable!(), + } + let _ = writeln!(out, ""); + } else if node.children.is_empty() && !has_properties { + let _ = writeln!(out, "/>"); + } else { + let _ = writeln!(out, ">"); + if has_fill_child { + write_fill(fill_emission, depth + 1, false, out)?; + } + for stroke in strokes { + write_stroke( + stroke, + &node.payload, + node.corner_smoothing, + depth + 1, + false, + out, + )?; + } + for c in &node.children { + print_node(doc, *c, depth + 1, dialect, out)?; + } + let _ = writeln!(out, "{indent}"); + } + Ok(()) +} diff --git a/crates/n0-model/tests/accessors.rs b/crates/n0-model/tests/accessors.rs new file mode 100644 index 00000000..18fac0b0 --- /dev/null +++ b/crates/n0-model/tests/accessors.rs @@ -0,0 +1,73 @@ +//! The non-panicking `Resolved` column reads (engine setup, step 2). The +//! damage differ and any consumer that walks all arena slots use these +//! instead of the `*_of` forms that assert resolution. Guards: they agree +//! with the panicking forms where resolved, and return `None` off the +//! resolved set (hidden subtree, out-of-range id). + +mod common; + +use common::opts_visual; +use n0_model::model::*; +use n0_model::resolve::resolve; + +#[test] +fn opt_accessors_agree_with_panicking_forms() { + let mut b = DocBuilder::new(); + let s = b.add( + 0, + Header::new(SizeIntent::Fixed(40.0), SizeIntent::Fixed(28.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let doc = b.build(); + let r = resolve(&doc, &opts_visual()); + + assert_eq!(r.box_opt(s), Some(r.box_of(s))); + assert_eq!(r.local_opt(s), Some(r.local_of(s))); + assert_eq!(r.aabb_opt(s), Some(r.aabb_of(s))); + assert_eq!(r.world_opt(s), Some(r.world_of(s))); +} + +#[test] +fn slot_count_matches_arena_capacity() { + let mut b = DocBuilder::new(); + b.add( + 0, + Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let doc = b.build(); + let r = resolve(&doc, &opts_visual()); + assert_eq!(r.slot_count(), doc.capacity()); +} + +#[test] +fn hidden_and_out_of_range_read_none() { + let mut b = DocBuilder::new(); + let mut h = Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)); + h.active = false; // display:none — the resolver skips it, column stays None + let hidden = b.add( + 0, + h, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let doc = b.build(); + let r = resolve(&doc, &opts_visual()); + + assert_eq!(r.box_opt(hidden), None); + assert_eq!(r.local_opt(hidden), None); + assert_eq!(r.aabb_opt(hidden), None); + assert_eq!(r.world_opt(hidden), None); + + // Never-allocated id: `.get()` guards the index rather than panicking. + let ghost: NodeId = 9999; + assert_eq!(r.box_opt(ghost), None); + assert_eq!(r.local_opt(ghost), None); + assert_eq!(r.aabb_opt(ghost), None); + assert_eq!(r.world_opt(ghost), None); +} diff --git a/crates/n0-model/tests/animation.rs b/crates/n0-model/tests/animation.rs new file mode 100644 index 00000000..d7efdcfd --- /dev/null +++ b/crates/n0-model/tests/animation.rs @@ -0,0 +1,2017 @@ +use n0_model::animation::{ + AnimationProgram, AnimationValueError, AnimationValueOperation, CompositeOperation, + CubicBezier, CubicBezierError, CubicControl, Easing, Endpoint, FillMode, + IterationCompositeOperation, KeyframeOffset, KeyframeOffsetError, ProgramError, SampleError, + SampleTime, ScalarCurve, ScalarCurveError, ScalarDomainError, ScalarKeyframe, ScalarSegment, + Timing, TimingError, Track, TrackEffectKind, TrackError, TrackKind, TransformCurve, + TransformCurveError, TransformKeyframe, TransformSegment, TransformValue, UnderlyingValueShape, +}; +use n0_model::model::{ + AxisBinding, DocBuilder, Document, Header, LensOp, Payload, ShapeDesc, SizeIntent, +}; +use n0_model::properties::{ + PropertyError, PropertyKey, PropertyTarget, PropertyValue, PropertyValues, +}; + +fn scene() -> (Document, u32, u32) { + let mut builder = DocBuilder::new(); + let first = builder.add( + 0, + Header::new(SizeIntent::Fixed(80.0), SizeIntent::Fixed(60.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let second = builder.add( + 0, + Header::new(SizeIntent::Fixed(40.0), SizeIntent::Fixed(30.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + (builder.build(), first, second) +} + +fn lens_scene(ops: Vec) -> (Document, u32) { + let mut builder = DocBuilder::new(); + let lens = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { ops }, + ); + builder.add( + lens, + Header::new(SizeIntent::Fixed(20.0), SizeIntent::Fixed(20.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + (builder.build(), lens) +} + +fn target(document: &Document, node: u32, property: PropertyKey) -> PropertyTarget { + PropertyTarget::new(document.key_of(node).expect("live node"), property) +} + +fn sampled_number(values: &PropertyValues, target: PropertyTarget) -> Option { + match values.get(target) { + Some(PropertyValue::Number(value)) => Some(*value), + None => None, + value => panic!("expected sampled number, found {value:?}"), + } +} + +fn sampled_axis(values: &PropertyValues, target: PropertyTarget) -> Option { + match values.get(target) { + Some(PropertyValue::AxisBinding(AxisBinding::Pin { offset, .. })) => Some(*offset), + None => None, + value => panic!("expected sampled start axis, found {value:?}"), + } +} + +fn sampled_size(values: &PropertyValues, target: PropertyTarget) -> Option { + match values.get(target) { + Some(PropertyValue::SizeIntent(SizeIntent::Fixed(value))) => Some(*value), + None => None, + value => panic!("expected sampled fixed size, found {value:?}"), + } +} + +fn offset(numerator: u64, denominator: u64) -> KeyframeOffset { + KeyframeOffset::new(numerator, denominator).unwrap() +} + +fn keyframe(numerator: u64, denominator: u64, value: f32) -> ScalarKeyframe { + ScalarKeyframe::new(offset(numerator, denominator), value) +} + +fn segment(easing: Easing, numerator: u64, denominator: u64, value: f32) -> ScalarSegment { + ScalarSegment::new(easing, keyframe(numerator, denominator, value)) +} + +#[test] +fn sample_time_and_timing_are_checked_integer_nanoseconds() { + let negative = SampleTime::from_nanoseconds(-17); + assert_eq!(negative.nanoseconds(), -17); + assert_eq!( + negative.checked_add_nanoseconds(20), + Some(SampleTime::from_nanoseconds(3)) + ); + assert_eq!( + SampleTime::from_nanoseconds(i64::MAX).checked_add_nanoseconds(1), + None + ); + assert_eq!( + SampleTime::from_nanoseconds(i64::MIN).checked_sub_nanoseconds(1), + None + ); + assert!(SampleTime::try_from(i128::from(i64::MAX) + 1).is_err()); + + assert_eq!(Timing::new(0, 0, 1), Err(TimingError::ZeroDuration)); + assert_eq!(Timing::new(0, 1, 0), Err(TimingError::ZeroRepeatCount)); + assert!(matches!( + Timing::new(i64::MAX, 1, 1), + Err(TimingError::ActiveEndOverflow { .. }) + )); + + let timing = Timing::new(20, 7, 3).unwrap(); + assert_eq!(timing.begin(), SampleTime::from_nanoseconds(20)); + assert_eq!(timing.duration_nanoseconds(), 7); + assert_eq!(timing.repeat_count(), 3); + assert_eq!(timing.active_end(), SampleTime::from_nanoseconds(41)); + + let negative = Timing::new(-20, 7, 3).unwrap(); + assert_eq!(negative.begin(), SampleTime::from_nanoseconds(-20)); + assert_eq!(negative.active_end(), SampleTime::from_nanoseconds(1)); + + let full_signed_span = Timing::new(i64::MIN, u64::MAX, 1).unwrap(); + assert_eq!( + full_signed_span.active_end(), + SampleTime::from_nanoseconds(i64::MAX) + ); + assert!(matches!( + Timing::new(i64::MIN, u64::MAX, 2), + Err(TimingError::ActiveEndOverflow { .. }) + )); +} + +#[test] +fn signed_timing_samples_before_and_across_the_timeline_origin() { + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let timing = Timing::new(-10, 20, 1).unwrap(); + let program = AnimationProgram::new( + &document, + "signed-timeline@0", + vec![Track::axis_start("move", x, 0.0, 20.0, timing, FillMode::Freeze).unwrap()], + ) + .unwrap(); + + let at = |nanoseconds| { + program + .sample(&document, SampleTime::from_nanoseconds(nanoseconds)) + .unwrap() + }; + assert_eq!(sampled_axis(&at(-11), x), None); + assert_eq!(sampled_axis(&at(-10), x), Some(0.0)); + assert_eq!(sampled_axis(&at(0), x), Some(10.0)); + assert_eq!(sampled_axis(&at(10), x), Some(20.0)); + + let extreme = AnimationProgram::new( + &document, + "full-signed-span@0", + vec![Track::axis_start( + "move", + x, + 0.0, + 1.0, + Timing::new(i64::MIN, u64::MAX, 1).unwrap(), + FillMode::Remove, + ) + .unwrap()], + ) + .unwrap(); + let midpoint = sampled_axis( + &extreme + .sample(&document, SampleTime::ZERO) + .expect("signed subtraction must not overflow"), + x, + ) + .unwrap(); + assert_eq!(midpoint.to_bits(), 0.5_f32.to_bits()); +} + +#[test] +fn track_constructors_close_property_and_endpoint_domains() { + let (document, rect, _) = scene(); + let timing = Timing::new(0, 10, 1).unwrap(); + let width = target(&document, rect, PropertyKey::Width); + let opacity = target(&document, rect, PropertyKey::Opacity); + + assert!(matches!( + Track::axis_start("wrong-property", width, 0.0, 1.0, timing, FillMode::Remove), + Err(TrackError::WrongProperty { + kind: TrackKind::AxisStart, + actual: PropertyKey::Width, + .. + }) + )); + assert!(matches!( + Track::fixed_size("negative", width, -1.0, 1.0, timing, FillMode::Remove), + Err(TrackError::InvalidEndpoint { + endpoint: Endpoint::From, + reason: ScalarDomainError::Negative, + .. + }) + )); + assert!(matches!( + Track::opacity("outside", opacity, 0.0, 1.1, timing, FillMode::Remove), + Err(TrackError::InvalidEndpoint { + endpoint: Endpoint::To, + reason: ScalarDomainError::OutsideUnitInterval, + .. + }) + )); + assert!(matches!( + Track::opacity("nan", opacity, f32::NAN, 1.0, timing, FillMode::Remove), + Err(TrackError::InvalidEndpoint { + endpoint: Endpoint::From, + reason: ScalarDomainError::NotFinite, + .. + }) + )); + assert_eq!( + Track::opacity("", opacity, 0.0, 1.0, timing, FillMode::Remove), + Err(TrackError::EmptySource) + ); +} + +#[test] +fn effect_accessors_and_composition_are_total_and_explicit() { + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let timing = Timing::new(0, 10, 1).unwrap(); + + let scalar = Track::axis_start("scalar", x, 0.0, 10.0, timing, FillMode::Remove).unwrap(); + assert_eq!(scalar.effect_kind(), TrackEffectKind::ScalarCurve); + assert!(scalar.scalar_curve().is_some()); + assert!(scalar.transform_curve().is_none()); + assert_eq!(scalar.composite(), CompositeOperation::Replace); + assert!(matches!( + scalar.clone().with_composition( + CompositeOperation::InterpolateLiveUnderlying, + IterationCompositeOperation::Replace, + ), + Err(TrackError::InvalidComposition { + effect: TrackEffectKind::ScalarCurve, + composite: CompositeOperation::InterpolateLiveUnderlying, + iteration_composite: IterationCompositeOperation::Replace, + .. + }) + )); + + let live = Track::axis_start_from_live_underlying( + "live", + x, + 10.0, + Easing::Linear, + timing, + FillMode::Remove, + ) + .unwrap(); + assert_eq!( + live.effect_kind(), + TrackEffectKind::ScalarFromLiveUnderlying + ); + assert!(live.scalar_curve().is_none()); + assert!(live.transform_curve().is_none()); + assert_eq!( + live.composite(), + CompositeOperation::InterpolateLiveUnderlying + ); + assert_eq!( + live.iteration_composite(), + IterationCompositeOperation::Replace + ); + live.clone() + .with_composition( + CompositeOperation::InterpolateLiveUnderlying, + IterationCompositeOperation::Replace, + ) + .unwrap(); + for (composite, iteration_composite) in [ + ( + CompositeOperation::Replace, + IterationCompositeOperation::Replace, + ), + ( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ), + ( + CompositeOperation::InterpolateLiveUnderlying, + IterationCompositeOperation::Accumulate, + ), + ] { + assert!(matches!( + live.clone() + .with_composition(composite, iteration_composite), + Err(TrackError::InvalidComposition { + effect: TrackEffectKind::ScalarFromLiveUnderlying, + .. + }) + )); + } + + let (lens_document, lens) = lens_scene(vec![]); + let lens_ops = target(&lens_document, lens, PropertyKey::LensOps); + let transform = Track::lens_transform_curve( + "transform", + lens_ops, + TransformCurve::linear( + TransformValue::Translate { x: 0.0, y: 0.0 }, + TransformValue::Translate { x: 10.0, y: 0.0 }, + ) + .unwrap(), + timing, + FillMode::Remove, + ) + .unwrap(); + assert_eq!(transform.effect_kind(), TrackEffectKind::TransformCurve); + assert!(transform.scalar_curve().is_none()); + assert!(transform.transform_curve().is_some()); + assert!(matches!( + transform.with_composition( + CompositeOperation::InterpolateLiveUnderlying, + IterationCompositeOperation::Replace, + ), + Err(TrackError::InvalidComposition { + effect: TrackEffectKind::TransformCurve, + .. + }) + )); +} + +#[test] +fn transform_constant_constructor_preserves_curve_validation() { + assert_eq!( + TransformCurve::constant(TransformValue::Translate { + x: f32::NAN, + y: 0.0, + }), + Err(TransformCurveError::NonFiniteValue { keyframe_index: 0 }) + ); + + let constant = TransformCurve::constant(TransformValue::Scale { x: 2.0, y: 3.0 }).unwrap(); + assert_eq!(constant.first().offset(), KeyframeOffset::ZERO); + assert_eq!(constant.keyframe_count(), 1); + assert_eq!( + constant.first_value(), + TransformValue::Scale { x: 2.0, y: 3.0 } + ); + + assert!(matches!( + TransformCurve::new( + TransformKeyframe::new( + KeyframeOffset::ZERO, + TransformValue::Translate { x: 0.0, y: 0.0 }, + ), + vec![TransformSegment::new( + Easing::Linear, + TransformKeyframe::new( + KeyframeOffset::ONE, + TransformValue::Scale { x: 1.0, y: 1.0 }, + ), + )], + ), + Err(TransformCurveError::MixedKinds { + expected: n0_model::animation::TransformKind::Translate, + keyframe_index: 1, + actual: n0_model::animation::TransformKind::Scale, + }) + )); + + let (document, lens) = lens_scene(vec![]); + let target = target(&document, lens, PropertyKey::LensOps); + let overflowing_pivot = TransformValue::Rotate { + degrees: 90.0, + center_x: f32::MAX, + center_y: f32::MAX, + }; + let curve = TransformCurve::linear(overflowing_pivot, overflowing_pivot).unwrap(); + assert!(matches!( + Track::lens_transform_curve( + "overflowing-pivot", + target, + curve, + Timing::new(0, 1, 1).unwrap(), + FillMode::Remove, + ), + Err(TrackError::InvalidTransformProjection { + keyframe_index: 0, + .. + }) + )); +} + +#[test] +fn offsets_are_exact_reduced_and_curves_have_one_canonical_structure() { + let half = KeyframeOffset::new(2, 4).unwrap(); + assert_eq!(half.numerator(), 1); + assert_eq!(half.denominator(), 2); + assert_eq!( + KeyframeOffset::new(0, u64::MAX).unwrap(), + KeyframeOffset::ZERO + ); + assert_eq!( + KeyframeOffset::new(u64::MAX, u64::MAX).unwrap(), + KeyframeOffset::ONE + ); + assert_eq!( + KeyframeOffset::new(1, 0), + Err(KeyframeOffsetError::ZeroDenominator { numerator: 1 }) + ); + assert_eq!( + KeyframeOffset::new(3, 2), + Err(KeyframeOffsetError::OutsideUnitInterval { + numerator: 3, + denominator: 2, + }) + ); + + assert!(matches!( + ScalarCurve::new( + ScalarKeyframe::new(half, 0.0), + vec![segment(Easing::Linear, 1, 1, 1.0)] + ), + Err(ScalarCurveError::FirstOffsetMustBeZero { actual }) if actual == half + )); + assert!(matches!( + ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![segment(Easing::Linear, 1, 2, 1.0)] + ), + Err(ScalarCurveError::LastOffsetMustBeOne { actual }) if actual == half + )); + assert!(matches!( + ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![ + segment(Easing::Linear, 1, 2, 1.0), + segment(Easing::Linear, 2, 4, 2.0), + segment(Easing::Linear, 1, 1, 3.0), + ] + ), + Err(ScalarCurveError::OffsetsNotStrictlyIncreasing { + previous_index: 1, + current_index: 2, + previous, + current, + }) if previous == half && current == half + )); + + let constant = ScalarCurve::new(ScalarKeyframe::new(half, 7.0), vec![]).unwrap(); + assert_eq!(constant, ScalarCurve::constant(7.0)); + assert_eq!(constant.first().offset(), KeyframeOffset::ZERO); + assert_eq!(constant.keyframe_count(), 1); +} + +#[test] +fn old_from_to_constructors_are_bit_equivalent_curve_sugar() { + let (document, rect, _) = scene(); + let target = target(&document, rect, PropertyKey::X); + let timing = Timing::new(3, 13, 2).unwrap(); + let old = AnimationProgram::new( + &document, + "old-sugar@0", + vec![Track::axis_start( + "old", + target, + f32::from_bits(0xbf80_0001), + f32::from_bits(0x3f00_0001), + timing, + FillMode::Freeze, + ) + .unwrap()], + ) + .unwrap(); + let explicit = AnimationProgram::new( + &document, + "explicit-curve@0", + vec![Track::axis_start_curve( + "explicit", + target, + ScalarCurve::linear(f32::from_bits(0xbf80_0001), f32::from_bits(0x3f00_0001)), + timing, + FillMode::Freeze, + ) + .unwrap()], + ) + .unwrap(); + + for time in [0, 3, 4, 7, 9, 15, 16, 22, 29, i64::MAX] { + let old = sampled_axis( + &old.sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + target, + ) + .map(f32::to_bits); + let explicit = sampled_axis( + &explicit + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + target, + ) + .map(f32::to_bits); + assert_eq!(old, explicit, "sample at {time}ns"); + } +} + +#[test] +fn uneven_offsets_interpolate_with_exact_local_progress() { + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let curve = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![ + segment(Easing::Linear, 1, 4, 100.0), + segment(Easing::Linear, 1, 1, 200.0), + ], + ) + .unwrap(); + let track = Track::axis_start_curve( + "uneven", + x, + curve, + Timing::new(0, 8, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "uneven@0", vec![track]).unwrap(); + + for (time, expected) in [ + (0, 0.0_f32), + (1, 50.0_f32), + (2, 100.0_f32), + (5, 150.0_f32), + (8, 200.0_f32), + ] { + let actual = sampled_axis( + &program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + x, + ) + .unwrap(); + assert_eq!(actual.to_bits(), expected.to_bits(), "sample at {time}ns"); + } +} + +#[test] +fn keyframe_repeat_and_freeze_boundaries_preserve_authored_bits() { + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let curve = ScalarCurve::new( + keyframe(0, 1, 2.0), + vec![ + segment(Easing::Linear, 1, 3, -0.0), + segment(Easing::Linear, 1, 1, 12.0), + ], + ) + .unwrap(); + let track = Track::axis_start_curve( + "boundaries", + x, + curve, + Timing::new(0, 12, 2).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "boundaries@0", vec![track]).unwrap(); + + for time in [4, 16] { + let value = sampled_axis( + &program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + x, + ) + .unwrap(); + assert_eq!(value.to_bits(), (-0.0_f32).to_bits()); + } + assert_eq!( + sampled_axis( + &program + .sample(&document, SampleTime::from_nanoseconds(12)) + .unwrap(), + x, + ) + .unwrap() + .to_bits(), + 2.0_f32.to_bits(), + "a repeat boundary returns the authored first keyframe" + ); + for time in [24, 25, i64::MAX] { + assert_eq!( + sampled_axis( + &program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + x, + ) + .unwrap() + .to_bits(), + 12.0_f32.to_bits(), + "freeze returns the authored last keyframe" + ); + } +} + +#[test] +fn cubic_identity_is_linear_and_exact_inverse_hits_return_exact_y() { + let (document, rect, _) = scene(); + let opacity = target(&document, rect, PropertyKey::Opacity); + let timing = Timing::new(0, 14, 1).unwrap(); + let identity = CubicBezier::new(0.25, 0.25, 0.75, 0.75).unwrap(); + let identity_curve = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![segment(Easing::CubicBezier(identity), 1, 1, 1.0)], + ) + .unwrap(); + let linear = AnimationProgram::new( + &document, + "linear@0", + vec![Track::opacity("linear", opacity, 0.0, 1.0, timing, FillMode::Freeze).unwrap()], + ) + .unwrap(); + let identity = AnimationProgram::new( + &document, + "identity@0", + vec![Track::opacity_curve( + "identity", + opacity, + identity_curve, + timing, + FillMode::Freeze, + ) + .unwrap()], + ) + .unwrap(); + for time in 0..=14 { + let linear_value = sampled_number( + &linear + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + opacity, + ) + .unwrap(); + let identity_value = sampled_number( + &identity + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + opacity, + ) + .unwrap(); + assert_eq!(linear_value.to_bits(), identity_value.to_bits()); + } + + // Bx(1/2) is exactly 1/2 for x controls (0, 1), so inversion hits the + // first dyadic midpoint exactly. With y controls (1, 1), By(1/2) = 7/8. + let exact_hit = CubicBezier::new(0.0, 1.0, 1.0, 1.0).unwrap(); + let curve = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![segment(Easing::CubicBezier(exact_hit), 1, 1, 1.0)], + ) + .unwrap(); + let program = AnimationProgram::new( + &document, + "exact-cubic-hit@0", + vec![Track::opacity_curve( + "ease", + opacity, + curve, + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap()], + ) + .unwrap(); + let value = sampled_number( + &program + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(), + opacity, + ) + .unwrap(); + assert_eq!(value.to_bits(), 0.875_f32.to_bits()); + + // A non-dyadic inverse exercises the full 128-step bracket. This golden + // bit pattern is the once-rounded value for CSS `ease` at progress 1/3. + let css_ease = CubicBezier::new(0.25, 0.1, 0.25, 1.0).unwrap(); + let curve = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![segment(Easing::CubicBezier(css_ease), 1, 1, 1.0)], + ) + .unwrap(); + let program = AnimationProgram::new( + &document, + "css-ease@0", + vec![Track::opacity_curve( + "ease", + opacity, + curve, + Timing::new(0, 3, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap()], + ) + .unwrap(); + let value = sampled_number( + &program + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(), + opacity, + ) + .unwrap(); + assert_eq!(value.to_bits(), 0x3f13_6bb6); +} + +#[test] +fn cubic_controls_and_property_space_hulls_are_validated() { + assert_eq!( + CubicBezier::new(f32::NAN, 0.0, 1.0, 1.0), + Err(CubicBezierError::NotFinite { + control: CubicControl::X1, + }) + ); + assert_eq!( + CubicBezier::new(0.0, 0.0, 1.1, 1.0), + Err(CubicBezierError::XOutsideUnitInterval { + control: CubicControl::X2, + }) + ); + assert_eq!( + CubicBezier::new(0.0, f32::INFINITY, 1.0, 1.0), + Err(CubicBezierError::NotFinite { + control: CubicControl::Y1, + }) + ); + + let (document, rect, _) = scene(); + let timing = Timing::new(0, 10, 1).unwrap(); + let opacity = target(&document, rect, PropertyKey::Opacity); + let opacity_overshoot = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![segment( + Easing::CubicBezier(CubicBezier::new(0.0, -0.25, 1.0, 1.0).unwrap()), + 1, + 1, + 1.0, + )], + ) + .unwrap(); + assert!(matches!( + Track::opacity_curve( + "opacity-overshoot", + opacity, + opacity_overshoot, + timing, + FillMode::Remove, + ), + Err(TrackError::UnsafeCubicControl { + segment_index: 0, + control: CubicControl::Y1, + reason: ScalarDomainError::OutsideUnitInterval, + .. + }) + )); + + let width = target(&document, rect, PropertyKey::Width); + let negative_size_control = ScalarCurve::new( + keyframe(0, 1, 10.0), + vec![segment( + Easing::CubicBezier(CubicBezier::new(0.0, -2.0, 1.0, 1.0).unwrap()), + 1, + 1, + 20.0, + )], + ) + .unwrap(); + assert!(matches!( + Track::fixed_size_curve( + "negative-size-control", + width, + negative_size_control, + timing, + FillMode::Remove, + ), + Err(TrackError::UnsafeCubicControl { + control: CubicControl::Y1, + reason: ScalarDomainError::Negative, + .. + }) + )); + + let x = target(&document, rect, PropertyKey::X); + let overflowing_axis_control = ScalarCurve::new( + keyframe(0, 1, -f32::MAX), + vec![segment( + Easing::CubicBezier(CubicBezier::new(0.0, f32::MAX, 1.0, 0.0).unwrap()), + 1, + 1, + f32::MAX, + )], + ) + .unwrap(); + assert!(matches!( + Track::axis_start_curve( + "overflowing-axis-control", + x, + overflowing_axis_control, + timing, + FillMode::Remove, + ), + Err(TrackError::UnsafeCubicControl { + control: CubicControl::Y1, + reason: ScalarDomainError::OutsideFiniteBinary32, + .. + }) + )); +} + +#[test] +fn every_curve_keyframe_is_validated_for_its_track_kind() { + let (document, rect, _) = scene(); + let opacity = target(&document, rect, PropertyKey::Opacity); + let curve = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![ + segment(Easing::Linear, 1, 2, 1.25), + segment(Easing::Linear, 1, 1, 1.0), + ], + ) + .unwrap(); + assert!(matches!( + Track::opacity_curve( + "bad-middle", + opacity, + curve, + Timing::new(0, 10, 1).unwrap(), + FillMode::Remove, + ), + Err(TrackError::InvalidKeyframe { + keyframe_index: 1, + reason: ScalarDomainError::OutsideUnitInterval, + .. + }) + )); +} + +#[test] +fn one_value_curves_are_constant_through_repeats_and_freeze() { + let (document, rect, _) = scene(); + let opacity = target(&document, rect, PropertyKey::Opacity); + let track = Track::opacity_curve( + "constant", + opacity, + ScalarCurve::constant(-0.0), + Timing::new(3, 5, 2).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let curve = track.scalar_curve().expect("the track owns a scalar curve"); + assert_eq!(curve.first_value().to_bits(), (-0.0_f32).to_bits()); + assert_eq!(curve.last_value().to_bits(), (-0.0_f32).to_bits()); + let program = AnimationProgram::new(&document, "constant@0", vec![track]).unwrap(); + + assert_eq!( + sampled_number( + &program + .sample(&document, SampleTime::from_nanoseconds(2)) + .unwrap(), + opacity, + ), + None + ); + for time in [3, 4, 7, 8, 12, 13, i64::MAX] { + let value = sampled_number( + &program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + opacity, + ) + .unwrap(); + assert_eq!(value.to_bits(), (-0.0_f32).to_bits()); + } +} + +#[test] +fn program_is_document_bound_target_major_and_preserves_stack_priority() { + let (document, rect, _) = scene(); + let timing = Timing::new(0, 10, 1).unwrap(); + let x = target(&document, rect, PropertyKey::X); + let opacity = target(&document, rect, PropertyKey::Opacity); + let program = AnimationProgram::new( + &document, + "test-profile@0", + vec![ + Track::axis_start( + "x-lower-later-begin", + x, + 0.0, + 10.0, + Timing::new(20, 20, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(), + Track::opacity("opacity", opacity, 1.0, 0.0, timing, FillMode::Freeze).unwrap(), + Track::axis_start( + "x-higher-earlier-begin", + x, + 20.0, + 30.0, + Timing::new(10, 40, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(), + ], + ) + .unwrap(); + assert_eq!(program.compiler_id(), "test-profile@0"); + assert_eq!(program.tracks()[0].target(), x); + assert_eq!(program.tracks()[0].source(), "x-lower-later-begin"); + assert_eq!(program.tracks()[1].target(), x); + assert_eq!(program.tracks()[1].source(), "x-higher-earlier-begin"); + assert_eq!(program.tracks()[2].target(), opacity); + let stacks = program.effect_stacks().collect::>(); + assert_eq!(stacks.len(), 2); + assert_eq!(stacks[0].len(), 2); + assert_eq!(stacks[1].len(), 1); + assert_eq!( + sampled_axis( + &program + .sample(&document, SampleTime::from_nanoseconds(25)) + .unwrap(), + x, + ), + Some(23.75), + "the kernel preserves frontend priority instead of deriving it from begin time" + ); + assert_eq!( + program.document_root(), + document.key_of(document.root).unwrap() + ); + assert!(matches!( + AnimationProgram::empty(&document, ""), + Err(ProgramError::EmptyCompilerId) + )); +} + +#[test] +fn replacement_stack_selects_the_highest_contributor_and_falls_through() { + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let lower = Track::axis_start( + "lower", + x, + 10.0, + 50.0, + Timing::new(10, 40, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let middle = Track::axis_start( + "middle", + x, + 70.0, + 110.0, + Timing::new(20, 20, 1).unwrap(), + FillMode::Remove, + ) + .unwrap(); + let higher = Track::axis_start( + "higher", + x, + 200.0, + 210.0, + Timing::new(25, 5, 1).unwrap(), + FillMode::Remove, + ) + .unwrap(); + let program = AnimationProgram::new( + &document, + "replacement-stack@0", + vec![lower, middle, higher], + ) + .unwrap(); + + let expected = [ + (9, None), + (10, Some(10.0)), + (19, Some(19.0)), + (20, Some(70.0)), + (25, Some(200.0)), + (29, Some(208.0)), + (30, Some(90.0)), + (40, Some(40.0)), + (50, Some(50.0)), + (i64::MAX, Some(50.0)), + ]; + for (time, expected) in expected { + let values = program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(); + assert_eq!(sampled_axis(&values, x), expected, "sample at {time}ns"); + assert!(values.len() <= 1, "one stack emits at most one value"); + } + + let frozen_higher = AnimationProgram::new( + &document, + "replacement-freeze@0", + vec![ + Track::axis_start( + "lower", + x, + 10.0, + 50.0, + Timing::new(10, 40, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(), + Track::axis_start( + "higher", + x, + 200.0, + 210.0, + Timing::new(25, 5, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(), + ], + ) + .unwrap(); + for time in [30, 50, i64::MAX] { + assert_eq!( + sampled_axis( + &frozen_higher + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + x, + ), + Some(210.0) + ); + } +} + +#[test] +fn scalar_composition_supports_all_effect_and_iteration_combinations() { + let (mut document, rect, _) = scene(); + document.get_mut(rect).header.x = AxisBinding::start(10.0); + let x = target(&document, rect, PropertyKey::X); + let timing = Timing::new(0, 10, 3).unwrap(); + + let cases = [ + ( + CompositeOperation::Replace, + IterationCompositeOperation::Replace, + [20.0, 25.0, 20.0, 25.0, 20.0, 25.0, 30.0], + ), + ( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + [30.0, 35.0, 30.0, 35.0, 30.0, 35.0, 40.0], + ), + ( + CompositeOperation::Replace, + IterationCompositeOperation::Accumulate, + [20.0, 25.0, 50.0, 55.0, 80.0, 85.0, 90.0], + ), + ( + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + [30.0, 35.0, 60.0, 65.0, 90.0, 95.0, 100.0], + ), + ]; + + for (composite, iteration, expected) in cases { + let track = Track::axis_start( + format!("{composite:?}-{iteration:?}"), + x, + 20.0, + 30.0, + timing, + FillMode::Freeze, + ) + .unwrap() + .with_composition(composite, iteration) + .unwrap(); + let program = + AnimationProgram::new(&document, "composition-matrix@0", vec![track]).unwrap(); + for (time, expected) in [0, 5, 10, 15, 20, 25, 30].into_iter().zip(expected) { + let values = program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(); + assert_eq!(sampled_axis(&values, x), Some(expected), "at {time}ns"); + } + } +} + +#[test] +fn cumulative_fixed_size_uses_the_last_keyframe_not_the_curve_delta() { + let (mut document, rect, _) = scene(); + document.get_mut(rect).header.width = SizeIntent::Fixed(20.0); + let width = target(&document, rect, PropertyKey::Width); + let curve = ScalarCurve::new( + keyframe(0, 1, 0.0), + vec![ + segment(Easing::Linear, 1, 2, 15.0), + segment(Easing::Linear, 1, 1, 10.0), + ], + ) + .unwrap(); + let track = Track::fixed_size_curve( + "cumulative-width", + width, + curve, + Timing::new(0, 20, 3).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "cumulative-width@0", vec![track]).unwrap(); + + for (time, expected) in [ + (0, 20.0), + (10, 35.0), + (20, 30.0), + (30, 45.0), + (40, 40.0), + (50, 55.0), + (60, 50.0), + ] { + assert_eq!( + sampled_size( + &program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + width, + ), + Some(expected), + "at {time}ns" + ); + } +} + +#[test] +fn additive_sandwich_accumulates_then_composes_and_replacement_cuts_lower_layers() { + let (mut document, rect, _) = scene(); + document.get_mut(rect).header.x = AxisBinding::start(8.0); + let x = target(&document, rect, PropertyKey::X); + let composed = |track: Result, composite, iteration| { + track + .unwrap() + .with_composition(composite, iteration) + .unwrap() + }; + let foundation = composed( + Track::axis_start( + "foundation", + x, + 2.0, + 6.0, + Timing::new(0, 10, 3).unwrap(), + FillMode::Freeze, + ), + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + ); + let replacement = composed( + Track::axis_start( + "replacement", + x, + 20.0, + 60.0, + Timing::new(25, 40, 1).unwrap(), + FillMode::Freeze, + ), + CompositeOperation::Replace, + IterationCompositeOperation::Replace, + ); + let persistent = composed( + Track::axis_start( + "persistent", + x, + 0.0, + 8.0, + Timing::new(30, 10, 2).unwrap(), + FillMode::Freeze, + ), + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + ); + let temporary = composed( + Track::axis_start( + "temporary", + x, + 4.0, + 12.0, + Timing::new(35, 10, 1).unwrap(), + FillMode::Remove, + ), + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ); + let program = AnimationProgram::new( + &document, + "additive-sandwich@0", + vec![foundation, replacement, persistent, temporary], + ) + .unwrap(); + + for (time, expected) in [ + (0, 10.0), + (5, 12.0), + (10, 16.0), + (15, 18.0), + (20, 22.0), + (25, 20.0), + (30, 25.0), + (35, 38.0), + (40, 51.0), + (45, 52.0), + (50, 61.0), + (55, 66.0), + (60, 71.0), + (65, 76.0), + (70, 76.0), + ] { + let values = program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(); + assert_eq!(sampled_axis(&values, x), Some(expected), "at {time}ns"); + } +} + +#[test] +fn typed_composition_rounds_per_layer_clamps_opacity_once_and_skips_masked_overflow() { + let (mut document, rect, _) = scene(); + document.get_mut(rect).header.x = AxisBinding::start(1.0); + document.get_mut(rect).header.opacity = 0.2; + let x = target(&document, rect, PropertyKey::X); + let opacity = target(&document, rect, PropertyKey::Opacity); + let active = Timing::new(0, 10, 1).unwrap(); + let half_ulp = 2.0_f32.powi(-24); + let add = |source, target, value| { + Track::axis_start(source, target, value, value, active, FillMode::Freeze) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap() + }; + let rounded = AnimationProgram::new( + &document, + "ordered-rounding@0", + vec![add("first", x, half_ulp), add("second", x, half_ulp)], + ) + .unwrap(); + assert_eq!( + sampled_axis(&rounded.sample(&document, SampleTime::ZERO).unwrap(), x), + Some(1.0), + "two additions are rounded in sandwich order, not regrouped" + ); + + let opacity_add = |source, value| { + Track::opacity(source, opacity, value, value, active, FillMode::Freeze) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap() + }; + let opacity_program = AnimationProgram::new( + &document, + "late-opacity-clamp@0", + vec![opacity_add("a", 0.4), opacity_add("b", 0.5)], + ) + .unwrap(); + assert_eq!( + sampled_number( + &opacity_program.sample(&document, SampleTime::ZERO).unwrap(), + opacity + ), + Some(1.0) + ); + + document.get_mut(rect).header.x = AxisBinding::start(f32::MAX); + let overflow = add("overflow", x, f32::MAX); + let failing = AnimationProgram::new(&document, "overflow@0", vec![overflow.clone()]).unwrap(); + assert!(matches!( + failing.sample(&document, SampleTime::ZERO), + Err(SampleError::InvalidComposition { + error: AnimationValueError::NonFiniteResult { .. }, + .. + }) + )); + let masked = Track::axis_start("mask", x, 7.0, 7.0, active, FillMode::Freeze).unwrap(); + let program = + AnimationProgram::new(&document, "masked-overflow@0", vec![overflow, masked]).unwrap(); + assert_eq!( + sampled_axis(&program.sample(&document, SampleTime::ZERO).unwrap(), x), + Some(7.0) + ); +} + +#[test] +fn composition_arithmetic_errors_report_only_applied_sources() { + let (mut document, rect, _) = scene(); + document.get_mut(rect).header.x = AxisBinding::start(f32::MAX); + let x = target(&document, rect, PropertyKey::X); + let timing = Timing::new(0, 10, 1).unwrap(); + let additive = |source, value| { + Track::axis_start(source, x, value, value, timing, FillMode::Freeze) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap() + }; + let program = AnimationProgram::new( + &document, + "causal-add-error@0", + vec![ + additive("overflowing-add", f32::MAX), + additive("not-yet-applied", 0.0), + ], + ) + .unwrap(); + assert!(matches!( + program.sample(&document, SampleTime::ZERO), + Err(SampleError::InvalidComposition { + sources, + error: AnimationValueError::NonFiniteResult { + operation: AnimationValueOperation::Add, + .. + }, + .. + }) if sources == ["overflowing-add"] + )); + + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let overflowing_accumulation = Track::axis_start( + "overflowing-accumulation", + x, + f32::MAX, + f32::MAX, + Timing::new(0, 1, 2).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Replace, + IterationCompositeOperation::Accumulate, + ) + .unwrap(); + let later_add = Track::axis_start( + "not-yet-applied", + x, + 0.0, + 0.0, + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let program = AnimationProgram::new( + &document, + "causal-accumulation-error@0", + vec![overflowing_accumulation, later_add], + ) + .unwrap(); + assert!(matches!( + program.sample(&document, SampleTime::from_nanoseconds(1)), + Err(SampleError::InvalidComposition { + sources, + error: AnimationValueError::NonFiniteResult { + operation: AnimationValueOperation::Accumulate, + .. + }, + .. + }) if sources == ["overflowing-accumulation"] + )); +} + +#[test] +fn additive_tracks_require_authored_numeric_underlying_values() { + let (mut document, rect, _) = scene(); + document.get_mut(rect).header.x = AxisBinding::center(12.0); + let x = target(&document, rect, PropertyKey::X); + let additive = Track::axis_start( + "add", + x, + 1.0, + 2.0, + Timing::new(0, 10, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + assert!(matches!( + AnimationProgram::new(&document, "bad-underlying@0", vec![additive.clone()]), + Err(ProgramError::InvalidComposition { + error: AnimationValueError::UnsupportedUnderlying { + actual: UnderlyingValueShape::CenterPin, + .. + }, + .. + }) + )); + + document.get_mut(rect).header.width = SizeIntent::Auto; + let width = target(&document, rect, PropertyKey::Width); + let additive_width = Track::fixed_size( + "add-width", + width, + 1.0, + 2.0, + Timing::new(0, 10, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + assert!(matches!( + AnimationProgram::new(&document, "auto-underlying@0", vec![additive_width]), + Err(ProgramError::InvalidComposition { + error: AnimationValueError::UnsupportedUnderlying { + actual: UnderlyingValueShape::AutoSize, + .. + }, + .. + }) + )); + document.get_mut(rect).header.width = SizeIntent::Fixed(80.0); + + let replacement = Track::axis_start( + "replace", + x, + 1.0, + 2.0, + Timing::new(0, 10, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + AnimationProgram::new( + &document, + "structural-base-replacement@0", + vec![replacement], + ) + .unwrap(); + + document.get_mut(rect).header.x = AxisBinding::start(12.0); + let program = AnimationProgram::new(&document, "mutated-underlying@0", vec![additive]).unwrap(); + document.get_mut(rect).header.x = AxisBinding::end(12.0); + assert!(matches!( + program.sample(&document, SampleTime::ZERO), + Err(SampleError::InvalidComposition { + error: AnimationValueError::UnsupportedUnderlying { + actual: UnderlyingValueShape::EndPin, + .. + }, + .. + }) + )); +} + +#[test] +fn additive_underlying_scalars_are_domain_checked_at_construction_and_sample() { + let timing = Timing::new(0, 10, 1).unwrap(); + let make_track = |source, target, kind| { + let track = match kind { + TrackKind::AxisStart => { + Track::axis_start(source, target, 1.0, 2.0, timing, FillMode::Freeze) + } + TrackKind::FixedSize => { + Track::fixed_size(source, target, 1.0, 2.0, timing, FillMode::Freeze) + } + TrackKind::Opacity => { + Track::opacity(source, target, 0.1, 0.2, timing, FillMode::Freeze) + } + TrackKind::SolidFill | TrackKind::LensTransform | TrackKind::PathGeometry => { + unreachable!("this table covers scalar track kinds") + } + }; + track + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap() + }; + let set_underlying = |document: &mut Document, node, kind, value| match kind { + TrackKind::AxisStart => document.get_mut(node).header.x = AxisBinding::start(value), + TrackKind::FixedSize => document.get_mut(node).header.width = SizeIntent::Fixed(value), + TrackKind::Opacity => document.get_mut(node).header.opacity = value, + TrackKind::SolidFill | TrackKind::LensTransform | TrackKind::PathGeometry => { + unreachable!("this table covers scalar track kinds") + } + }; + + for (kind, property, invalid, expected) in [ + ( + TrackKind::AxisStart, + PropertyKey::X, + f32::INFINITY, + ScalarDomainError::NotFinite, + ), + ( + TrackKind::FixedSize, + PropertyKey::Width, + -1.0, + ScalarDomainError::Negative, + ), + ( + TrackKind::Opacity, + PropertyKey::Opacity, + 1.25, + ScalarDomainError::OutsideUnitInterval, + ), + ] { + let (mut document, rect, _) = scene(); + set_underlying(&mut document, rect, kind, invalid); + let authored_target = target(&document, rect, property); + match AnimationProgram::new( + &document, + "invalid-authored-underlying@0", + vec![make_track("compile-add", authored_target, kind)], + ) + .unwrap_err() + { + ProgramError::InvalidComposition { + sources, + error: + AnimationValueError::InvalidUnderlying { + target: actual, + kind: actual_kind, + reason, + }, + } => { + assert_eq!(sources, ["compile-add"]); + assert_eq!(actual, authored_target); + assert_eq!(actual_kind, kind); + assert_eq!(reason, expected); + } + error => panic!("expected invalid authored underlying scalar, found {error:?}"), + } + + let (mut document, rect, _) = scene(); + let mutated_target = target(&document, rect, property); + let program = AnimationProgram::new( + &document, + "invalid-mutated-underlying@0", + vec![make_track("sample-add", mutated_target, kind)], + ) + .unwrap(); + set_underlying(&mut document, rect, kind, invalid); + match program.sample(&document, SampleTime::ZERO).unwrap_err() { + SampleError::InvalidComposition { + sources, + error: + AnimationValueError::InvalidUnderlying { + target: actual, + kind: actual_kind, + reason, + }, + .. + } => { + assert_eq!(sources, ["sample-add"]); + assert_eq!(actual, mutated_target); + assert_eq!(actual_kind, kind); + assert_eq!(reason, expected); + } + error => panic!("expected invalid mutated underlying scalar, found {error:?}"), + } + } +} + +#[test] +fn additive_transform_underlying_is_checked_at_construction_even_when_masked() { + let (document, lens) = lens_scene(vec![LensOp::Translate { + x: f32::NAN, + y: 0.0, + }]); + let lens_ops = target(&document, lens, PropertyKey::LensOps); + let timing = Timing::new(0, 10, 1).unwrap(); + let curve = TransformCurve::linear( + TransformValue::Translate { x: 0.0, y: 0.0 }, + TransformValue::Translate { x: 10.0, y: 0.0 }, + ) + .unwrap(); + let additive = Track::lens_transform_curve( + "add-transform", + lens_ops, + curve.clone(), + timing, + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let replacement = Track::lens_transform_curve( + "replace-transform", + lens_ops, + curve, + timing, + FillMode::Freeze, + ) + .unwrap(); + + for tracks in [ + vec![additive.clone()], + vec![additive.clone(), replacement.clone()], + ] { + assert!(matches!( + AnimationProgram::new(&document, "invalid-lens-underlying@0", tracks), + Err(ProgramError::InvalidValues { + sources, + error: PropertyError::InvalidValue { target, .. }, + }) if sources == ["add-transform"] && target == lens_ops + )); + } + + // Replacement never consumes the authored list, matching the existing + // scalar replacement cutoff contract. + AnimationProgram::new( + &document, + "replacement-quarantines-underlying@0", + vec![replacement], + ) + .unwrap(); +} + +#[test] +fn mutated_additive_transform_underlying_is_checked_while_inactive_and_masked() { + let (mut document, lens) = lens_scene(vec![]); + let lens_ops = target(&document, lens, PropertyKey::LensOps); + let curve = TransformCurve::linear( + TransformValue::Translate { x: 0.0, y: 0.0 }, + TransformValue::Translate { x: 10.0, y: 0.0 }, + ) + .unwrap(); + let additive = Track::lens_transform_curve( + "inactive-add-transform", + lens_ops, + curve.clone(), + Timing::new(100, 10, 1).unwrap(), + FillMode::Remove, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let replacement = Track::lens_transform_curve( + "higher-replacement", + lens_ops, + curve, + Timing::new(0, 10, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new( + &document, + "mutated-lens-underlying@0", + vec![additive, replacement], + ) + .unwrap(); + + match &mut document.get_mut(lens).payload { + Payload::Lens { ops } => ops.push(LensOp::Rotate { deg: f32::INFINITY }), + _ => unreachable!("test node is a lens"), + } + + assert!(matches!( + program.sample(&document, SampleTime::from_nanoseconds(50)), + Err(SampleError::InvalidValues { + sources, + error: PropertyError::InvalidValue { target, .. }, + .. + }) if sources == ["inactive-add-transform"] && target == lens_ops + )); +} + +#[test] +fn empty_program_is_static_at_every_time_but_refuses_another_arena() { + let (document, _, _) = scene(); + let program = AnimationProgram::empty(&document, "empty@0").unwrap(); + for time in [i64::MIN, -1, 0, 1, i64::MAX] { + assert!(program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap() + .is_empty()); + } + + let clone = document.clone(); + assert!(matches!( + program.sample(&clone, SampleTime::ZERO), + Err(SampleError::DocumentMismatch { .. }) + )); +} + +#[test] +fn repeats_remove_and_freeze_have_exact_boundary_behavior() { + let (document, rect, _) = scene(); + let x = target(&document, rect, PropertyKey::X); + let timing = Timing::new(10, 10, 2).unwrap(); + let remove = AnimationProgram::new( + &document, + "remove@0", + vec![Track::axis_start("move", x, 2.0, 12.0, timing, FillMode::Remove).unwrap()], + ) + .unwrap(); + + let at = |nanoseconds| { + remove + .sample(&document, SampleTime::from_nanoseconds(nanoseconds)) + .unwrap() + }; + assert_eq!(sampled_axis(&at(9), x), None); + assert_eq!( + sampled_axis(&at(10), x).unwrap().to_bits(), + 2.0_f32.to_bits() + ); + assert_eq!( + sampled_axis(&at(15), x).unwrap().to_bits(), + 7.0_f32.to_bits() + ); + assert_eq!( + sampled_axis(&at(20), x).unwrap().to_bits(), + 2.0_f32.to_bits(), + "an internal repeat boundary begins the next iteration at progress zero" + ); + assert_eq!(sampled_axis(&at(30), x), None); + assert_eq!(sampled_axis(&at(i64::MAX), x), None); + + let freeze = AnimationProgram::new( + &document, + "freeze@0", + vec![Track::axis_start("move", x, 2.0, -0.0, timing, FillMode::Freeze).unwrap()], + ) + .unwrap(); + for time in [30, 31, i64::MAX] { + assert_eq!( + sampled_axis( + &freeze + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + x, + ) + .unwrap() + .to_bits(), + (-0.0_f32).to_bits(), + "freeze returns the authored terminal endpoint bits" + ); + } +} + +fn midpoint_value(kind: TrackKind, from: f32, to: f32) -> f32 { + let (document, rect, _) = scene(); + let timing = Timing::new(0, 2, 1).unwrap(); + let (target, track) = match kind { + TrackKind::AxisStart => { + let target = target(&document, rect, PropertyKey::X); + ( + target, + Track::axis_start("tie", target, from, to, timing, FillMode::Freeze).unwrap(), + ) + } + TrackKind::FixedSize => { + let target = target(&document, rect, PropertyKey::Width); + ( + target, + Track::fixed_size("tie", target, from, to, timing, FillMode::Freeze).unwrap(), + ) + } + TrackKind::Opacity => { + let target = target(&document, rect, PropertyKey::Opacity); + ( + target, + Track::opacity("tie", target, from, to, timing, FillMode::Freeze).unwrap(), + ) + } + TrackKind::SolidFill | TrackKind::LensTransform | TrackKind::PathGeometry => { + unreachable!("midpoint_value covers scalar track kinds") + } + }; + let program = AnimationProgram::new(&document, "rounding@0", vec![track]).unwrap(); + let values = program + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(); + match kind { + TrackKind::AxisStart => sampled_axis(&values, target).unwrap(), + TrackKind::FixedSize => sampled_size(&values, target).unwrap(), + TrackKind::Opacity => sampled_number(&values, target).unwrap(), + TrackKind::SolidFill | TrackKind::LensTransform | TrackKind::PathGeometry => { + unreachable!("midpoint_value covers scalar track kinds") + } + } +} + +#[test] +fn rational_interpolation_rounds_once_to_binary32_ties_even() { + let even = f32::from_bits(0x3f00_0000); + let odd = f32::from_bits(0x3f00_0001); + let next_even = f32::from_bits(0x3f00_0002); + assert_eq!( + midpoint_value(TrackKind::Opacity, even, odd).to_bits(), + even.to_bits(), + "a halfway result chooses the even lower significand" + ); + assert_eq!( + midpoint_value(TrackKind::Opacity, odd, next_even).to_bits(), + next_even.to_bits(), + "a halfway result chooses the even upper significand" + ); + + let min_subnormal = f32::from_bits(1); + let even_subnormal = f32::from_bits(2); + assert_eq!( + midpoint_value(TrackKind::FixedSize, 0.0, min_subnormal).to_bits(), + 0.0_f32.to_bits() + ); + assert_eq!( + midpoint_value(TrackKind::FixedSize, min_subnormal, even_subnormal).to_bits(), + even_subnormal.to_bits() + ); + let largest_subnormal = f32::from_bits(0x007f_ffff); + let minimum_normal = f32::from_bits(0x0080_0000); + assert_eq!( + midpoint_value(TrackKind::FixedSize, largest_subnormal, minimum_normal,).to_bits(), + minimum_normal.to_bits(), + "rounding carries cleanly from the subnormal to normal encoding" + ); + + let negative_even = f32::from_bits(0xbf80_0000); + let negative_odd = f32::from_bits(0xbf80_0001); + assert_eq!( + midpoint_value(TrackKind::AxisStart, negative_even, negative_odd).to_bits(), + negative_even.to_bits(), + "ties-to-even is symmetric for negative values" + ); + assert_eq!( + midpoint_value(TrackKind::AxisStart, -min_subnormal, -0.0).to_bits(), + (-0.0_f32).to_bits(), + "a negative halfway underflow retains the IEEE negative-zero sign" + ); + + let below_max = f32::from_bits(0x7f7f_fffe); + assert_eq!( + midpoint_value(TrackKind::AxisStart, below_max, f32::MAX).to_bits(), + below_max.to_bits(), + "rounding near the finite ceiling does not overflow" + ); +} + +#[test] +fn a_stale_track_rejects_the_whole_sample_with_source_identity() { + let (mut document, first, second) = scene(); + let timing = Timing::new(0, 10, 1).unwrap(); + let x = target(&document, first, PropertyKey::X); + let opacity = target(&document, second, PropertyKey::Opacity); + let program = AnimationProgram::new( + &document, + "atomic@0", + vec![ + Track::axis_start("still-live", x, 0.0, 10.0, timing, FillMode::Freeze).unwrap(), + Track::opacity("removed", opacity, 1.0, 0.0, timing, FillMode::Freeze).unwrap(), + ], + ) + .unwrap(); + document.remove_subtree(second); + + let error = program + .sample(&document, SampleTime::from_nanoseconds(5)) + .unwrap_err(); + assert!(matches!( + error, + SampleError::InvalidValues { sources, .. } if sources == ["removed"] + )); +} + +#[test] +fn a_masked_stale_stack_reports_every_source() { + let (mut document, first, _) = scene(); + let x = target(&document, first, PropertyKey::X); + let program = AnimationProgram::new( + &document, + "stale-stack@0", + vec![ + Track::axis_start( + "lower-inactive", + x, + 0.0, + 10.0, + Timing::new(100, 10, 1).unwrap(), + FillMode::Remove, + ) + .unwrap(), + Track::axis_start( + "higher-frozen", + x, + 20.0, + 30.0, + Timing::new(0, 10, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(), + ], + ) + .unwrap(); + document.remove_subtree(first); + + let error = program + .sample(&document, SampleTime::from_nanoseconds(50)) + .unwrap_err(); + assert!(matches!( + error, + SampleError::InvalidValues { sources, .. } + if sources == ["lower-inactive", "higher-frozen"] + )); +} + +#[test] +fn inactive_tracks_still_refuse_stale_or_inapplicable_targets() { + let (mut document, first, second) = scene(); + let delayed = Timing::new(10, 10, 1).unwrap(); + let width = target(&document, first, PropertyKey::Width); + let opacity = target(&document, second, PropertyKey::Opacity); + let program = AnimationProgram::new( + &document, + "inactive-validity@0", + vec![ + Track::fixed_size( + "becomes-inapplicable", + width, + 80.0, + 100.0, + delayed, + FillMode::Remove, + ) + .unwrap(), + Track::opacity( + "becomes-stale", + opacity, + 1.0, + 0.0, + delayed, + FillMode::Remove, + ) + .unwrap(), + ], + ) + .unwrap(); + + document.remove_subtree(second); + for time in [0, 20, i64::MAX] { + let error = program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap_err(); + assert!(matches!( + error, + SampleError::InvalidValues { sources, .. } if sources == ["becomes-stale"] + )); + } + + // Restore a separate program so the stale target cannot mask the + // applicability check. + let (mut document, first, _) = scene(); + let width = target(&document, first, PropertyKey::Width); + let program = AnimationProgram::new( + &document, + "inactive-applicability@0", + vec![Track::fixed_size( + "becomes-inapplicable", + width, + 80.0, + 100.0, + delayed, + FillMode::Remove, + ) + .unwrap()], + ) + .unwrap(); + document.get_mut(first).payload = Payload::Group; + for time in [0, 20, i64::MAX] { + let error = program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap_err(); + assert!(matches!( + error, + SampleError::InvalidValues { sources, .. } if sources == ["becomes-inapplicable"] + )); + } +} diff --git a/crates/n0-model/tests/animation_path_curves.rs b/crates/n0-model/tests/animation_path_curves.rs new file mode 100644 index 00000000..2b8ef627 --- /dev/null +++ b/crates/n0-model/tests/animation_path_curves.rs @@ -0,0 +1,424 @@ +//! Format-neutral smooth and discrete path-geometry animation conformance. + +use std::sync::Arc; + +use n0_model::animation::{ + AnimationProgram, CubicBezier, CubicControl, DiscreteCurve, DiscreteCurveError, + DiscreteKeyframe, Easing, FillMode, KeyframeOffset, PathCurve, PathCurveError, PathKeyframe, + PathSegment, SampleTime, Timing, Track, TrackError, +}; +use n0_model::model::{DocBuilder, Document, FillRule, Header, Payload, ShapeDesc, SizeIntent}; +use n0_model::path::{self, PathCommand, PathGeometry}; +use n0_model::properties::{PropertyKey, PropertyTarget, PropertyValue, PropertyValues}; +use n0_model::resolve::{resolve_view, ResolveOptions}; + +fn geometry(commands: Vec) -> Arc { + PathGeometry::from_commands(commands).unwrap() +} + +fn triangle(tip_x: f32) -> Arc { + geometry(vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::LineTo { x: 1.0, y: 0.0 }, + PathCommand::LineTo { x: tip_x, y: 1.0 }, + PathCommand::Close, + ]) +} + +fn scene() -> (Document, PropertyTarget) { + let mut builder = DocBuilder::new(); + let authored = path::analyze("M0 0 L1 0 L0 1 Z", FillRule::NonZero).unwrap(); + let node = builder.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + Payload::Shape { + desc: ShapeDesc::Path(authored), + }, + ); + let document = builder.build(); + let target = PropertyTarget::new( + document.key_of(node).expect("live path"), + PropertyKey::PathGeometry, + ); + (document, target) +} + +fn sampled_path(values: &PropertyValues, target: PropertyTarget) -> Option<&Arc> { + match values.get(target) { + Some(PropertyValue::PathGeometry(path)) => Some(path), + None => None, + value => panic!("expected sampled path geometry, found {value:?}"), + } +} + +#[test] +fn smooth_path_curve_preserves_exact_endpoints_and_interpolates_every_command_component() { + let from = geometry(vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::LineTo { x: 0.25, y: 0.0 }, + PathCommand::QuadTo { + x1: 0.25, + y1: 0.25, + x: 0.5, + y: 0.0, + }, + PathCommand::CubicTo { + x1: 0.25, + y1: 0.25, + x2: 0.5, + y2: 0.5, + x: 0.75, + y: 0.0, + }, + PathCommand::Close, + ]); + let to = geometry(vec![ + PathCommand::MoveTo { x: 0.5, y: 0.5 }, + PathCommand::LineTo { x: 0.75, y: 1.0 }, + PathCommand::QuadTo { + x1: 0.75, + y1: 0.75, + x: 1.0, + y: 1.0, + }, + PathCommand::CubicTo { + x1: 0.75, + y1: 0.75, + x2: 1.0, + y2: 1.0, + x: 0.25, + y: 1.0, + }, + PathCommand::Close, + ]); + let (document, target) = scene(); + let track = Track::path_curve( + "smooth", + target, + PathCurve::linear(Arc::clone(&from), Arc::clone(&to)).unwrap(), + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "smooth-path@0", vec![track]).unwrap(); + + let at = |time| { + program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap() + }; + let start = at(0); + assert!(Arc::ptr_eq(sampled_path(&start, target).unwrap(), &from)); + + let midpoint = at(1); + assert_eq!( + sampled_path(&midpoint, target).unwrap().commands.as_ref(), + &[ + PathCommand::MoveTo { x: 0.25, y: 0.25 }, + PathCommand::LineTo { x: 0.5, y: 0.5 }, + PathCommand::QuadTo { + x1: 0.5, + y1: 0.5, + x: 0.75, + y: 0.5, + }, + PathCommand::CubicTo { + x1: 0.5, + y1: 0.5, + x2: 0.75, + y2: 0.75, + x: 0.5, + y: 0.5, + }, + PathCommand::Close, + ] + ); + + let frozen = at(2); + assert!(Arc::ptr_eq(sampled_path(&frozen, target).unwrap(), &to)); +} + +#[test] +fn smooth_path_curve_rejects_topology_conics_and_nonconvex_property_easing() { + let line = geometry(vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::LineTo { x: 1.0, y: 1.0 }, + ]); + let quad = geometry(vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::QuadTo { + x1: 0.5, + y1: 0.0, + x: 1.0, + y: 1.0, + }, + ]); + assert!(matches!( + PathCurve::linear(Arc::clone(&line), quad), + Err(PathCurveError::DifferentTopology { keyframe_index: 1 }) + )); + + let arc = path::analyze_geometry_in_reference_box("M0 0 A1 1 0 0 1 1 1", 1.0, 1.0).unwrap(); + assert!(matches!( + PathCurve::linear(Arc::clone(&arc), arc), + Err(PathCurveError::ConicNotInterpolable { + keyframe_index: 0, + .. + }) + )); + + let arc = path::analyze_geometry_in_reference_box("M0 0 A1 1 0 0 1 1 1", 1.0, 1.0).unwrap(); + assert!(matches!( + PathCurve::constant(arc), + Err(PathCurveError::ConicNotInterpolable { + keyframe_index: 0, + .. + }) + )); + + let nonconvex = Easing::CubicBezier(CubicBezier::new(0.25, -0.1, 0.75, 1.1).unwrap()); + assert!(matches!( + PathCurve::new( + PathKeyframe::new(KeyframeOffset::ZERO, Arc::clone(&line)), + vec![PathSegment::new( + nonconvex, + PathKeyframe::new(KeyframeOffset::ONE, line), + )], + ), + Err(PathCurveError::UnsafeCubicControl { + segment_index: 0, + control: CubicControl::Y1, + }) + )); +} + +#[test] +fn explicit_discrete_curve_switches_on_equal_offsets_and_holds_a_terminal_below_one() { + let first = triangle(0.0); + let second = triangle(0.5); + let third = triangle(1.0); + let curve = DiscreteCurve::new(vec![ + DiscreteKeyframe::new( + KeyframeOffset::ZERO, + PropertyValue::PathGeometry(Arc::clone(&first)), + ), + DiscreteKeyframe::new( + KeyframeOffset::new(1, 4).unwrap(), + PropertyValue::PathGeometry(Arc::clone(&second)), + ), + DiscreteKeyframe::new( + KeyframeOffset::new(3, 4).unwrap(), + PropertyValue::PathGeometry(Arc::clone(&third)), + ), + ]) + .unwrap(); + let (document, target) = scene(); + let track = Track::path_discrete_curve( + "discrete", + target, + curve, + Timing::new(0, 8, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "discrete-path@0", vec![track]).unwrap(); + + for (time, expected) in [ + (0, &first), + (1, &first), + (2, &second), + (5, &second), + (6, &third), + (7, &third), + (8, &third), + ] { + let values = program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(); + assert!( + Arc::ptr_eq(sampled_path(&values, target).unwrap(), expected), + "unexpected held value at {time}ns" + ); + } +} + +#[test] +fn incompatible_pair_switches_after_easing_not_at_raw_half_progress() { + let from = triangle(0.0); + let to = geometry(vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::QuadTo { + x1: 0.5, + y1: 0.0, + x: 1.0, + y: 1.0, + }, + ]); + let (document, target) = scene(); + let track = Track::path_discrete_fallback( + "eased-fallback", + target, + Arc::clone(&from), + Arc::clone(&to), + Easing::CubicBezier(CubicBezier::new(0.42, 0.0, 1.0, 1.0).unwrap()), + Timing::new(0, 16, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "eased-fallback@0", vec![track]).unwrap(); + + let at_half = program + .sample(&document, SampleTime::from_nanoseconds(8)) + .unwrap(); + assert!( + Arc::ptr_eq(sampled_path(&at_half, target).unwrap(), &from), + "ease-in remains below the discrete threshold at raw half progress" + ); + let at_three_quarters = program + .sample(&document, SampleTime::from_nanoseconds(12)) + .unwrap(); + assert!(Arc::ptr_eq( + sampled_path(&at_three_quarters, target).unwrap(), + &to + )); +} + +#[test] +fn sampled_geometry_uses_the_current_authored_fill_rule() { + let from = triangle(0.0); + let to = triangle(1.0); + let (mut document, target) = scene(); + let track = Track::path_curve( + "fill-rule-independent-geometry", + target, + PathCurve::linear(from, to).unwrap(), + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "fill-rule@0", vec![track]).unwrap(); + + let node = document.get(document.root).children[0]; + let Payload::Shape { + desc: ShapeDesc::Path(path), + } = &mut document.get_mut(node).payload + else { + panic!("scene helper creates a path") + }; + Arc::make_mut(path).set_fill_rule(FillRule::EvenOdd); + + let values = program + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(); + let view = n0_model::properties::ValueView::new(&document, &values).unwrap(); + let resolved = resolve_view(&view, &ResolveOptions::default()); + assert_eq!(resolved.resolved_path_of(node).fill_rule, FillRule::EvenOdd); +} + +#[test] +fn replacement_sandwich_falls_through_to_lower_freeze_or_keeps_higher_freeze() { + let base = triangle(0.0); + let lower_to = triangle(0.25); + let higher_from = triangle(0.75); + let higher_to = triangle(1.0); + let (document, target) = scene(); + let lower = Track::path_curve( + "lower", + target, + PathCurve::linear(Arc::clone(&base), Arc::clone(&lower_to)).unwrap(), + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let higher = |fill| { + Track::path_curve( + "higher", + target, + PathCurve::linear(Arc::clone(&higher_from), Arc::clone(&higher_to)).unwrap(), + Timing::new(1, 1, 1).unwrap(), + fill, + ) + .unwrap() + }; + + let removing = AnimationProgram::new( + &document, + "path-sandwich-remove@0", + vec![lower.clone(), higher(FillMode::Remove)], + ) + .unwrap(); + let during_higher = removing + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(); + assert!(Arc::ptr_eq( + sampled_path(&during_higher, target).unwrap(), + &higher_from + )); + let after_higher = removing + .sample(&document, SampleTime::from_nanoseconds(2)) + .unwrap(); + assert!( + Arc::ptr_eq(sampled_path(&after_higher, target).unwrap(), &lower_to), + "the removed higher replacement exposes the lower frozen value" + ); + + let freezing = AnimationProgram::new( + &document, + "path-sandwich-freeze@0", + vec![lower, higher(FillMode::Freeze)], + ) + .unwrap(); + let after_both = freezing + .sample(&document, SampleTime::from_nanoseconds(2)) + .unwrap(); + assert!( + Arc::ptr_eq(sampled_path(&after_both, target).unwrap(), &higher_to), + "the higher frozen replacement continues to cut the lower value" + ); +} + +#[test] +fn discrete_curve_rejects_mixed_complete_value_kinds() { + let path = triangle(0.5); + assert!(matches!( + DiscreteCurve::new(vec![ + DiscreteKeyframe::new(KeyframeOffset::ZERO, PropertyValue::PathGeometry(path),), + DiscreteKeyframe::new(KeyframeOffset::ONE, PropertyValue::Number(1.0)), + ]), + Err(DiscreteCurveError::MixedValueKind { + keyframe_index: 1, + .. + }) + )); +} + +#[test] +fn path_discrete_track_revalidates_public_geometry_metadata() { + let valid = triangle(0.5); + let mut malformed = valid.as_ref().clone(); + malformed.all_contours_closed = !malformed.all_contours_closed; + let malformed = Arc::new(malformed); + let curve = DiscreteCurve::new(vec![ + DiscreteKeyframe::new( + KeyframeOffset::ZERO, + PropertyValue::PathGeometry(Arc::clone(&valid)), + ), + DiscreteKeyframe::new(KeyframeOffset::ONE, PropertyValue::PathGeometry(malformed)), + ]) + .unwrap(); + let (_document, target) = scene(); + + assert!(matches!( + Track::path_discrete_curve( + "malformed-geometry", + target, + curve, + Timing::new(0, 1, 1).unwrap(), + FillMode::Remove, + ), + Err(TrackError::InvalidPathGeometry { + keyframe_index: 1, + .. + }) + )); +} diff --git a/crates/n0-model/tests/animation_profile5_paints.rs b/crates/n0-model/tests/animation_profile5_paints.rs new file mode 100644 index 00000000..5402d80b --- /dev/null +++ b/crates/n0-model/tests/animation_profile5_paints.rs @@ -0,0 +1,505 @@ +//! Format-neutral Profile 5 solid-fill effect conformance. + +use n0_model::animation::{ + AnimationProgram, AnimationValueError, ColorCurve, ColorCurveError, ColorKeyframe, + ColorSegment, CompositeOperation, Easing, FillMode, IterationCompositeOperation, + KeyframeOffset, ProgramError, SampleError, SampleTime, Timing, Track, TrackEffectKind, + TrackError, TrackKind, UnderlyingValueShape, MAX_SOLID_FILL_EFFECTS_PER_TARGET, +}; +use n0_model::model::{ + BlendMode, Color, DocBuilder, Document, Header, ImagePaint, Paint, Paints, Payload, ShapeDesc, + SizeIntent, SolidPaint, +}; +use n0_model::properties::{PropertyKey, PropertyTarget, PropertyValue, PropertyValues}; + +fn scene(fills: Paints) -> (Document, u32) { + let mut builder = DocBuilder::new(); + let rect = builder.add( + 0, + Header::new(SizeIntent::Fixed(32.0), SizeIntent::Fixed(24.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + builder.node_mut(rect).fills = fills; + (builder.build(), rect) +} + +fn target(document: &Document, node: u32, property: PropertyKey) -> PropertyTarget { + PropertyTarget::new(document.key_of(node).expect("live node"), property) +} + +fn sampled_color(values: &PropertyValues, target: PropertyTarget) -> Option { + match values.get(target) { + Some(PropertyValue::Paints(paints)) => match paints.as_slice() { + [Paint::Solid(paint)] if paint.active && paint.blend_mode == BlendMode::Normal => { + Some(paint.color) + } + value => panic!("expected one active normal solid paint, found {value:?}"), + }, + None => None, + value => panic!("expected sampled fills, found {value:?}"), + } +} + +#[test] +fn color_curve_rejects_every_noncanonical_offset_topology() { + let color = Color::BLACK; + let half = KeyframeOffset::new(1, 2).unwrap(); + let quarter = KeyframeOffset::new(1, 4).unwrap(); + + assert!(matches!( + ColorCurve::new( + ColorKeyframe::new(quarter, color), + vec![ColorSegment::new( + Easing::Linear, + ColorKeyframe::new(KeyframeOffset::ONE, color), + )], + ), + Err(ColorCurveError::FirstOffsetMustBeZero { actual }) if actual == quarter + )); + assert!(matches!( + ColorCurve::new( + ColorKeyframe::new(KeyframeOffset::ZERO, color), + vec![ColorSegment::new( + Easing::Linear, + ColorKeyframe::new(half, color), + )], + ), + Err(ColorCurveError::LastOffsetMustBeOne { actual }) if actual == half + )); + assert!(matches!( + ColorCurve::new( + ColorKeyframe::new(KeyframeOffset::ZERO, color), + vec![ + ColorSegment::new( + Easing::Linear, + ColorKeyframe::new(half, color), + ), + ColorSegment::new( + Easing::Linear, + ColorKeyframe::new(half, color), + ), + ], + ), + Err(ColorCurveError::OffsetsNotStrictlyIncreasing { + previous_index: 1, + current_index: 2, + previous, + current, + }) if previous == half && current == half + )); +} + +#[test] +fn color_curves_are_typed_complete_fill_values_and_preserve_curve_structure() { + let quarter = KeyframeOffset::new(1, 4).unwrap(); + let curve = ColorCurve::new( + ColorKeyframe::new(KeyframeOffset::ZERO, Color(0x0000_0000)), + vec![ + ColorSegment::new( + Easing::Linear, + ColorKeyframe::new(quarter, Color(0x4040_2000)), + ), + ColorSegment::new( + Easing::Linear, + ColorKeyframe::new(KeyframeOffset::ONE, Color(0xFFFD_0000)), + ), + ], + ) + .unwrap(); + assert_eq!(curve.keyframe_count(), 3); + assert_eq!(curve.first_color(), Color(0x0000_0000)); + assert_eq!(curve.last_color(), Color(0xFFFD_0000)); + + // Replacement must be able to cut any authored fill aggregate. It emits + // one complete singleton-solid `Paints` value, not a nested color patch. + let authored = Paints::new([ + Paint::Solid(SolidPaint::new(Color(0xFF10_1828))), + Paint::Solid(SolidPaint::new(Color(0x8025_63EB))), + ]); + let (document, rect) = scene(authored); + let fills = target(&document, rect, PropertyKey::Fills); + let track = Track::solid_fill_curve( + "typed-fill", + fills, + curve, + Timing::new(10, 4, 1).unwrap(), + FillMode::Remove, + ) + .unwrap(); + assert_eq!(track.kind(), TrackKind::SolidFill); + assert_eq!(track.effect_kind(), TrackEffectKind::SolidFillCurve); + assert!(track.color_curve().is_some()); + assert!(track.scalar_curve().is_none()); + assert!(track.transform_curve().is_none()); + + let program = AnimationProgram::new(&document, "typed-fill@0", vec![track]).unwrap(); + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(10)) + .unwrap(), + fills, + ), + Some(Color(0x0000_0000)) + ); + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(11)) + .unwrap(), + fills, + ), + Some(Color(0x4040_2000)), + "the exact keyframe retains its authored RGBA8 value" + ); + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(14)) + .unwrap(), + fills, + ), + None, + "remove falls through to the authored two-paint aggregate" + ); +} + +#[test] +fn color_interpolation_is_unpremultiplied_and_quantizes_half_up_once() { + let (document, rect) = scene(Paints::solid(Color::BLACK)); + let fills = target(&document, rect, PropertyKey::Fills); + let program = AnimationProgram::new( + &document, + "rgba-midpoint@0", + vec![Track::solid_fill( + "rgba-midpoint", + fills, + Color(0x0000_0000), + Color(0xFDFD_0000), + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap()], + ) + .unwrap(); + + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(), + fills, + ), + Some(Color(0x7F7F_0000)), + "alpha and red interpolate independently; 126.5 rounds to 127" + ); +} + +#[test] +fn color_sandwich_retains_fractional_channels_through_addition_and_accumulation() { + let (document, rect) = scene(Paints::solid(Color(0xFF10_0000))); + let fills = target(&document, rect, PropertyKey::Fills); + let foundation = Track::solid_fill( + "foundation", + fills, + Color(0xFF10_0000), + Color(0xFF12_0000), + Timing::new(0, 2, 2).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + ) + .unwrap(); + let half_step = Track::solid_fill( + "half-step", + fills, + Color(0x0000_0000), + Color(0x0001_0000), + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let program = AnimationProgram::new( + &document, + "exact-color-sandwich@0", + vec![foundation, half_step], + ) + .unwrap(); + + // This is the Chromium characterization sequence. At t=1 the two effects + // contribute 17 and 0.5 over authored 16: keeping exact channels through + // the sandwich gives 33.5 -> 34. Per-effect quantization would give 33. + for (time, red) in [(0, 32), (1, 34), (2, 51), (3, 52), (4, 53)] { + let expected = Color(0xFF00_0000 | (red << 16)); + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(time)) + .unwrap(), + fills, + ), + Some(expected), + "unexpected composed color at {time}ns" + ); + } +} + +#[test] +fn higher_live_effect_reads_unclamped_lower_color() { + let (document, rect) = scene(Paints::solid(Color(0xFFF0_0000))); + let fills = target(&document, rect, PropertyKey::Fills); + let lower = Track::solid_fill( + "overflowing-lower", + fills, + Color(0x0020_0000), + Color(0x0020_0000), + Timing::new(0, 4, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let higher = Track::solid_fill_from_live_underlying( + "live-higher", + fills, + Color::BLACK, + Easing::Linear, + Timing::new(0, 4, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let program = AnimationProgram::new(&document, "late-clamp@0", vec![lower, higher]).unwrap(); + + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(2)) + .unwrap(), + fills, + ), + Some(Color(0xFF88_0000)), + "the higher midpoint reads red 272 from below, yielding 136; clamping below first would yield 128" + ); +} + +#[test] +fn lone_to_reads_the_live_lower_color_and_addition_requires_a_compatible_solid_base() { + let (document, rect) = scene(Paints::solid(Color(0xFF10_2030))); + let fills = target(&document, rect, PropertyKey::Fills); + let lower = Track::solid_fill( + "lower", + fills, + Color(0xFF20_3040), + Color(0xFF40_5060), + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + let live = Track::solid_fill_from_live_underlying( + "live", + fills, + Color(0xFF80_90A0), + Easing::Linear, + Timing::new(0, 2, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap(); + assert_eq!( + live.effect_kind(), + TrackEffectKind::SolidFillFromLiveUnderlying + ); + let program = AnimationProgram::new(&document, "live-color@0", vec![lower, live]).unwrap(); + assert_eq!( + sampled_color( + &program + .sample(&document, SampleTime::from_nanoseconds(1)) + .unwrap(), + fills, + ), + Some(Color(0xFF58_6878)), + "the lone-to effect interpolates from the sampled lower replacement" + ); + + let (empty_document, empty_rect) = scene(Paints::default()); + let fills = target(&empty_document, empty_rect, PropertyKey::Fills); + let additive = Track::solid_fill( + "needs-solid-base", + fills, + Color::BLACK, + Color::BLACK, + Timing::new(0, 1, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + assert!(matches!( + AnimationProgram::new(&empty_document, "bad-fill-base@0", vec![additive]), + Err(ProgramError::InvalidComposition { + error: AnimationValueError::UnsupportedUnderlying { + expected: TrackKind::SolidFill, + actual: UnderlyingValueShape::EmptyPaints, + .. + }, + .. + }) + )); + + let opacity = target(&document, rect, PropertyKey::Opacity); + assert!(matches!( + Track::solid_fill( + "wrong-property", + opacity, + Color::BLACK, + Color::BLACK, + Timing::new(0, 1, 1).unwrap(), + FillMode::Remove, + ), + Err(TrackError::WrongProperty { + kind: TrackKind::SolidFill, + actual: PropertyKey::Opacity, + .. + }) + )); +} + +#[test] +fn additive_color_underlying_shape_is_checked_at_construction_and_sample_time() { + let mut inactive = SolidPaint::new(Color::BLACK); + inactive.active = false; + let mut blended = SolidPaint::new(Color::BLACK); + blended.blend_mode = BlendMode::Multiply; + let cases = [ + (Paints::default(), UnderlyingValueShape::EmptyPaints), + ( + Paints::new([Paint::Solid(inactive)]), + UnderlyingValueShape::InactiveSolidPaint, + ), + ( + Paints::new([Paint::Solid(blended)]), + UnderlyingValueShape::BlendedSolidPaint, + ), + ( + Paints::new([Paint::Image(ImagePaint::from_rid("texture"))]), + UnderlyingValueShape::NonSolidPaint, + ), + ( + Paints::new([ + Paint::Solid(SolidPaint::new(Color::BLACK)), + Paint::Solid(SolidPaint::new(Color(0xFFFFFFFF))), + ]), + UnderlyingValueShape::PaintStack, + ), + ]; + + for (paints, expected) in cases { + let (document, rect) = scene(paints); + let fills = target(&document, rect, PropertyKey::Fills); + let additive = Track::solid_fill( + "requires-compatible-solid", + fills, + Color::BLACK, + Color::BLACK, + Timing::new(0, 1, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let error = AnimationProgram::new(&document, "invalid-color-underlying@0", vec![additive]) + .unwrap_err(); + assert!(matches!( + &error, + ProgramError::InvalidComposition { + error: AnimationValueError::UnsupportedUnderlying { actual, .. }, + .. + } if *actual == expected + )); + assert!( + error + .to_string() + .contains("exactly one active, normal-blend solid paint"), + "{error}" + ); + } + + let (mut document, rect) = scene(Paints::solid(Color::BLACK)); + let fills = target(&document, rect, PropertyKey::Fills); + let additive = Track::solid_fill( + "mutated-base", + fills, + Color::BLACK, + Color::BLACK, + Timing::new(10, 1, 1).unwrap(), + FillMode::Remove, + ) + .unwrap() + .with_composition( + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ) + .unwrap(); + let program = + AnimationProgram::new(&document, "mutated-color-underlying@0", vec![additive]).unwrap(); + document.get_mut(rect).fills = Paints::new([ + Paint::Solid(SolidPaint::new(Color::BLACK)), + Paint::Solid(SolidPaint::new(Color(0xFFFFFFFF))), + ]); + assert!(matches!( + program.sample(&document, SampleTime::ZERO), + Err(SampleError::InvalidComposition { + sources, + error: AnimationValueError::UnsupportedUnderlying { + actual: UnderlyingValueShape::PaintStack, + .. + }, + .. + }) if sources == ["mutated-base"] + )); +} + +#[test] +fn exact_solid_fill_stacks_have_a_reported_complexity_limit() { + let (document, rect) = scene(Paints::solid(Color::BLACK)); + let fills = target(&document, rect, PropertyKey::Fills); + let tracks = (0..=MAX_SOLID_FILL_EFFECTS_PER_TARGET) + .map(|index| { + Track::solid_fill( + format!("effect-{index}"), + fills, + Color::BLACK, + Color::BLACK, + Timing::new(0, 1, 1).unwrap(), + FillMode::Freeze, + ) + .unwrap() + }) + .collect(); + assert!(matches!( + AnimationProgram::new(&document, "bounded-solid-fill@0", tracks), + Err(ProgramError::TooManySolidFillEffects { + count, + maximum: MAX_SOLID_FILL_EFFECTS_PER_TARGET, + .. + }) if count == MAX_SOLID_FILL_EFFECTS_PER_TARGET + 1 + )); +} diff --git a/crates/n0-model/tests/arena_pick.rs b/crates/n0-model/tests/arena_pick.rs new file mode 100644 index 00000000..f23b07ed --- /dev/null +++ b/crates/n0-model/tests/arena_pick.rs @@ -0,0 +1,250 @@ +//! Part-1 spike surface: the node arena's structural APIs, the delete op, +//! and hit-testing (`pick`) over the resolved SOA tier. + +use n0_model::model::*; +use n0_model::ops; +use n0_model::pick::pick; +use n0_model::resolve::{resolve, ResolveOptions}; + +fn opts() -> ResolveOptions { + ResolveOptions { + viewport: (1000.0, 800.0), + ..Default::default() + } +} + +fn card(w: f32, h: f32) -> (Header, Payload) { + ( + Header::new(SizeIntent::Fixed(w), SizeIntent::Fixed(h)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ) +} + +fn at(mut h: Header, x: f32, y: f32) -> Header { + h.x = AxisBinding::start(x); + h.y = AxisBinding::start(y); + h +} + +fn flex_row(w: f32, h: f32) -> (Header, Payload) { + ( + Header::new(SizeIntent::Fixed(w), SizeIntent::Fixed(h)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + gap_main: 10.0, + padding: EdgeInsets::all(10.0), + cross_align: CrossAlign::Center, + ..Default::default() + }, + clips_content: false, + }, + ) +} + +// ---- arena structural APIs ------------------------------------------- + +#[test] +fn parent_links_survive_structural_ops() { + let mut b = DocBuilder::new(); + let (fh, fp) = flex_row(460.0, 170.0); + let f = b.add(0, at(fh, 20.0, 20.0), fp); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(10.0); + gh.y = AxisBinding::start(10.0); + let g = b.add(0, gh, Payload::Group); + let (ch, cp) = card(40.0, 40.0); + let a = b.add(g, ch, cp); + let mut doc = b.build(); + + assert_eq!(doc.parent_of(f), Some(0)); + assert_eq!(doc.parent_of(a), Some(g)); + + // ungroup re-homes the child's parent link to the group's parent + let r = resolve(&doc, &opts()); + ops::ungroup(&mut doc, &r, g).unwrap(); + assert_eq!(doc.parent_of(a), Some(0)); + assert!(doc.get_opt(g).is_none()); +} + +#[test] +fn delete_removes_subtree_and_reflows() { + let mut b = DocBuilder::new(); + let (fh, fp) = flex_row(460.0, 170.0); + let f = b.add(0, at(fh, 20.0, 20.0), fp); + let mut ids = vec![]; + for _ in 0..3 { + let (ch, cp) = card(60.0, 100.0); + ids.push(b.add(f, ch, cp)); + } + let mut doc = b.build(); + let r0 = resolve(&doc, &opts()); + let slot1_x = r0.box_of(ids[1]).x; + + let n = ops::delete(&mut doc, ids[1]).unwrap(); + assert_eq!(n, 1); + assert!(doc.get_opt(ids[1]).is_none()); + + // the row reflows: c takes the deleted slot + let r1 = resolve(&doc, &opts()); + assert_eq!(r1.box_of(ids[2]).x, slot1_x); + + // the root is a wall + let root = doc.root; + assert_eq!(ops::delete(&mut doc, root), Err(ops::OpError::WrongKind)); + // and double-delete is typed, not a panic + assert_eq!(ops::delete(&mut doc, ids[1]), Err(ops::OpError::WrongKind)); +} + +#[test] +fn delete_is_subtree_deep() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(100.0); + gh.y = AxisBinding::start(100.0); + let g = b.add(0, gh, Payload::Group); + let (c1h, c1p) = card(40.0, 40.0); + let a = b.add(g, c1h, c1p); + let (mut c2h, c2p) = card(40.0, 40.0); + c2h.x = AxisBinding::start(60.0); + let s = b.add(g, c2h, c2p); + let mut doc = b.build(); + + let n = ops::delete(&mut doc, g).unwrap(); + assert_eq!(n, 3, "group + both members"); + for id in [g, a, s] { + assert!(doc.get_opt(id).is_none()); + } +} + +// ---- pick -------------------------------------------------------------- + +#[test] +fn pick_is_oriented_not_aabb() { + let mut b = DocBuilder::new(); + let (mut ch, cp) = card(120.0, 24.0); + ch = at(ch, 200.0, 200.0); + ch.rotation = 45.0; + let id = b.add(0, ch, cp); + let doc = b.build(); + let r = resolve(&doc, &opts()); + + // box center always hits + let (cx, cy) = (260.0, 212.0); + assert_eq!(pick(&r, cx, cy), Some(id)); + // a point inside the AABB but OUTSIDE the oriented box must miss + // (thin 45deg bar: the AABB corner region is empty space) + let aabb = r.aabb_of(id); + let corner = (aabb.x + 4.0, aabb.y + 4.0); + assert_eq!(pick(&r, corner.0, corner.1), Some(doc.root)); +} + +#[test] +fn pick_topmost_wins() { + let mut b = DocBuilder::new(); + let (c1, p1) = card(100.0, 100.0); + let under = b.add(0, at(c1, 100.0, 100.0), p1); + let (c2, p2) = card(100.0, 100.0); + let over = b.add(0, at(c2, 150.0, 150.0), p2); + let doc = b.build(); + let r = resolve(&doc, &opts()); + // overlap region: later sibling paints on top and wins the pick + assert_eq!(pick(&r, 175.0, 175.0), Some(over)); + // non-overlapped part of the lower one still hits it + assert_eq!(pick(&r, 110.0, 110.0), Some(under)); +} + +#[test] +fn pick_promotes_to_outermost_group() { + let mut b = DocBuilder::new(); + let mut outer_h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + outer_h.x = AxisBinding::start(100.0); + outer_h.y = AxisBinding::start(100.0); + let outer = b.add(0, outer_h, Payload::Group); + let inner_h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + let inner = b.add(outer, inner_h, Payload::Group); + let (ch, cp) = card(50.0, 50.0); + let leaf = b.add(inner, ch, cp); + let doc = b.build(); + let r = resolve(&doc, &opts()); + let _ = (inner, leaf); + // clicking the leaf's ink selects the OUTERMOST group + assert_eq!(pick(&r, 120.0, 120.0), Some(outer)); +} + +#[test] +fn pick_hits_lens_content_post_ops() { + let mut b = DocBuilder::new(); + let mut lh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + lh.x = AxisBinding::start(300.0); + lh.y = AxisBinding::start(300.0); + let l = b.add( + 0, + lh, + Payload::Lens { + ops: vec![LensOp::Translate { x: 200.0, y: 0.0 }], + }, + ); + let (ch, cp) = card(60.0, 60.0); + b.add(l, ch, cp); + let doc = b.build(); + let r = resolve(&doc, &opts()); + + // the PRE-ops position is empty space (ops moved the ink away)... + assert_eq!(pick(&r, 330.0, 330.0), Some(doc.root)); + // ...the POST-ops position hits, and promotes to the lens + assert_eq!(pick(&r, 530.0, 330.0), Some(l)); +} + +#[test] +fn pick_hairline_line_is_grabbable() { + let mut b = DocBuilder::new(); + let mut lh = Header::new(SizeIntent::Fixed(200.0), SizeIntent::Fixed(0.0)); + lh = at(lh, 100.0, 400.0); + let line = b.add( + 0, + lh, + Payload::Shape { + desc: ShapeDesc::Line, + }, + ); + let doc = b.build(); + let r = resolve(&doc, &opts()); + assert_eq!(pick(&r, 180.0, 401.5), Some(line)); +} + +#[test] +fn pick_respects_the_exact_transformed_clip_of_every_ancestor() { + let mut b = DocBuilder::new(); + let mut clip_header = Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)); + clip_header = at(clip_header, 200.0, 200.0); + clip_header.rotation = 45.0; + let clip = b.add( + 0, + clip_header, + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: true, + }, + ); + let (child_header, child_payload) = card(40.0, 40.0); + let child = b.add(clip, at(child_header, 80.0, 20.0), child_payload); + let doc = b.build(); + let r = resolve(&doc, &opts()); + + let inside_clip = r.world_of(child).apply((10.0, 10.0)); + assert_eq!( + pick(&r, inside_clip.0, inside_clip.1), + Some(child), + "the visible part of the child remains pickable" + ); + + let outside_clip = r.world_of(child).apply((30.0, 10.0)); + assert_eq!( + pick(&r, outside_clip.0, outside_clip.1), + Some(doc.root), + "the child is not pickable through its rotated ancestor clip" + ); +} diff --git a/crates/n0-model/tests/common/mod.rs b/crates/n0-model/tests/common/mod.rs new file mode 100644 index 00000000..927db2f9 --- /dev/null +++ b/crates/n0-model/tests/common/mod.rs @@ -0,0 +1,81 @@ +//! Shared helpers for the conformance-derived suites. + +use n0_model::math::RectF; +use n0_model::model::*; +use n0_model::resolve::{resolve, ResolveOptions, Resolved, RotationInFlow}; + +pub const EPS: f32 = 1e-3; // N-3 within-platform tolerance for the lab + +pub fn opts() -> ResolveOptions { + ResolveOptions { + viewport: (1000.0, 1000.0), + rotation_in_flow: RotationInFlow::AabbParticipates, + } +} + +pub fn opts_visual() -> ResolveOptions { + ResolveOptions { + viewport: (1000.0, 1000.0), + rotation_in_flow: RotationInFlow::VisualOnly, + } +} + +pub fn run(doc: &Document) -> Resolved { + resolve(doc, &opts()) +} + +pub fn assert_close(a: f32, b: f32, msg: &str) { + assert!( + (a - b).abs() < EPS, + "{msg}: {a} vs {b} (Δ={})", + (a - b).abs() + ); +} + +pub fn assert_rect(r: RectF, x: f32, y: f32, w: f32, h: f32, msg: &str) { + assert_close(r.x, x, &format!("{msg}.x")); + assert_close(r.y, y, &format!("{msg}.y")); + assert_close(r.w, w, &format!("{msg}.w")); + assert_close(r.h, h, &format!("{msg}.h")); +} + +pub fn shape(w: f32, h: f32) -> (Header, Payload) { + ( + Header::new(SizeIntent::Fixed(w), SizeIntent::Fixed(h)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ) +} + +pub fn frame_free(w: SizeIntent, h: SizeIntent) -> (Header, Payload) { + ( + Header::new(w, h), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ) +} + +pub fn frame_flex( + w: SizeIntent, + h: SizeIntent, + direction: Direction, + gap: f32, + padding: f32, +) -> (Header, Payload) { + ( + Header::new(w, h), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + direction, + gap_main: gap, + padding: EdgeInsets::all(padding), + ..Default::default() + }, + clips_content: false, + }, + ) +} diff --git a/crates/n0-model/tests/composition.rs b/crates/n0-model/tests/composition.rs new file mode 100644 index 00000000..428b068c --- /dev/null +++ b/crates/n0-model/tests/composition.rs @@ -0,0 +1,142 @@ +//! Boxed-node composition: an internal Shape may own free-positioned children +//! while keeping its parametric box and layout contribution independent. Text +//! stays a leaf. Draft 0 exposes this as direct primitive composition such as +//! ``. + +use n0_model::model::*; +use n0_model::resolve::{resolve, ResolveOptions}; + +fn rect(w: f32, h: f32) -> (Header, Payload) { + ( + Header::new(SizeIntent::Fixed(w), SizeIntent::Fixed(h)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ) +} + +#[test] +fn shape_children_resolve_in_the_shape_local_box() { + let mut builder = DocBuilder::new(); + let (mut shape_header, shape_payload) = rect(200.0, 100.0); + shape_header.x = AxisBinding::start(100.0); + shape_header.y = AxisBinding::start(50.0); + let shape = builder.add(0, shape_header, shape_payload); + + let mut text_header = Header::new(SizeIntent::Auto, SizeIntent::Auto); + text_header.x = AxisBinding::start(10.0); + text_header.y = AxisBinding::start(12.0); + let text = builder.add( + shape, + text_header, + Payload::Text { + content: "content".into(), + font_size: 10.0, + }, + ); + + let doc = builder.build(); + let resolved = resolve(&doc, &ResolveOptions::default()); + assert_eq!(resolved.xywh(shape), (100.0, 50.0, 200.0, 100.0)); + assert_eq!(resolved.box_of(text).x, 10.0); + assert_eq!(resolved.box_of(text).y, 12.0); + let world = resolved.world_of(text); + assert_eq!((world.e, world.f), (110.0, 62.0)); +} + +/// Children are paint/content, not a second measurement input. Overflow may +/// enlarge read-tier ink bounds, but it cannot resize the Shape or move its +/// flex sibling. +#[test] +fn shape_children_do_not_feed_back_into_box_or_layout() { + let mut builder = DocBuilder::new(); + let frame = builder.add( + 0, + Header::new(SizeIntent::Fixed(300.0), SizeIntent::Fixed(100.0)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + gap_main: 10.0, + ..Default::default() + }, + clips_content: false, + }, + ); + + let (shape_header, shape_payload) = rect(80.0, 60.0); + let composed = builder.add(frame, shape_header, shape_payload); + let mut overflow_header = Header::new(SizeIntent::Auto, SizeIntent::Auto); + overflow_header.x = AxisBinding::start(90.0); + let overflow = builder.add( + composed, + overflow_header, + Payload::Text { + content: "overflow".into(), + font_size: 10.0, + }, + ); + let (sibling_header, sibling_payload) = rect(40.0, 40.0); + let sibling = builder.add(frame, sibling_header, sibling_payload); + + let doc = builder.build(); + let resolved = resolve(&doc, &ResolveOptions::default()); + assert_eq!(resolved.box_of(composed).w, 80.0); + assert_eq!(resolved.box_of(sibling).x, 90.0, "80px box + 10px gap"); + assert_eq!(resolved.box_of(overflow).x, 90.0); + assert!( + resolved.aabb_of(composed).w > resolved.box_of(composed).w, + "read-tier ink still includes overflowing descendants" + ); +} + +#[test] +fn payload_child_capabilities_are_explicit() { + assert!(Payload::Shape { + desc: ShapeDesc::Rect + } + .accepts_children()); + assert!(Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + } + .accepts_children()); + assert!(!Payload::Text { + content: String::new(), + font_size: 16.0, + } + .accepts_children()); +} + +#[test] +#[should_panic(expected = "text does not accept children")] +fn builder_rejects_text_children() { + let mut builder = DocBuilder::new(); + let text = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Text { + content: "leaf".into(), + font_size: 16.0, + }, + ); + let (child_header, child_payload) = rect(10.0, 10.0); + builder.add(text, child_header, child_payload); +} + +#[test] +#[should_panic(expected = "text does not accept children")] +fn splice_rejects_text_children() { + let mut builder = DocBuilder::new(); + let text = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Text { + content: "leaf".into(), + font_size: 16.0, + }, + ); + let (header, payload) = rect(10.0, 10.0); + let child = builder.add(0, header, payload); + let mut doc = builder.build(); + doc.splice_children(text, 0, 0, vec![child]); +} diff --git a/crates/n0-model/tests/derived.rs b/crates/n0-model/tests/derived.rs new file mode 100644 index 00000000..e9662c9f --- /dev/null +++ b/crates/n0-model/tests/derived.rs @@ -0,0 +1,177 @@ +//! D-* — derived boxes (groups, lens): the P6 instability fixes. + +mod common; +use common::*; + +use n0_model::model::*; +use n0_model::resolve::resolve; + +fn rotated_group_doc() -> (Document, NodeId, NodeId, NodeId) { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(100.0); + gh.y = AxisBinding::start(50.0); + gh.rotation = 30.0; + let g = b.add(0, gh, Payload::Group); + let (s1h, s1p) = shape(40.0, 40.0); + let s1 = b.add(g, s1h, s1p); + let (mut s2h, s2p) = shape(40.0, 40.0); + s2h.x = AxisBinding::start(56.0); + let s2 = b.add(g, s2h, s2p); + (b.build(), g, s1, s2) +} + +/// D-1: group bounds = union of children's *oriented* corners. +#[test] +fn d1_union_of_oriented_corners() { + let mut b = DocBuilder::new(); + let g = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (mut s1h, s1p) = shape(100.0, 20.0); + s1h.rotation = 90.0; // oriented AABB: 20×100 about center (50,10) + let s1 = b.add(g, s1h, s1p); + let doc = b.build(); + let r = run(&doc); + let _ = s1; + // rotated about center (50,10): AABB x 40..60, y −40..60 + let u = r.box_of(g); + assert_close(u.w, 20.0, "union w from oriented corners"); + assert_close(u.h, 100.0, "union h from oriented corners"); +} + +/// D-2: editing child A of a rotated group does not move child B in world +/// space — the P6 instability test. +#[test] +fn d2_sibling_stability_under_rotated_group() { + let (doc, _, s1, s2) = rotated_group_doc(); + let r1 = run(&doc); + let s2_world_before = r1.world_of(s2); + let s2_corner_before = s2_world_before.apply((0.0, 0.0)); + + // edit child A: move it (grows the union leftward/upward) + let mut doc2 = doc.clone(); + doc2.get_mut(s1).header.x = AxisBinding::start(-30.0); + doc2.get_mut(s1).header.y = AxisBinding::start(-10.0); + let r2 = run(&doc2); + let s2_corner_after = r2.world_of(s2).apply((0.0, 0.0)); + + assert_close(s2_corner_before.0, s2_corner_after.0, "sibling B fixed x"); + assert_close(s2_corner_before.1, s2_corner_after.1, "sibling B fixed y"); +} + +/// D-3: child edits never require a write to the group node. +#[test] +fn d3_child_edit_writes_nothing_to_group() { + let (doc, g, s1, _) = rotated_group_doc(); + let mut doc2 = doc.clone(); + doc2.get_mut(s1).header.x = AxisBinding::start(-30.0); + // group node bytes identical + assert_eq!(doc.get(g), doc2.get(g), "group untouched by child edit"); +} + +/// D-6 (declared): a group participates in parent flex via its derived +/// AABB (rotated per the E1 flag). +#[test] +fn d6_group_in_flex_participates_via_derived_aabb() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(200.0), + Direction::Row, + 10.0, + 0.0, + ); + let f = b.add(0, h, p); + let (s0h, s0p) = shape(50.0, 50.0); + let s0 = b.add(f, s0h, s0p); + let g = b.add( + f, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (g1h, g1p) = shape(30.0, 30.0); + b.add(g, g1h, g1p); + let (mut g2h, g2p) = shape(30.0, 30.0); + g2h.x = AxisBinding::start(40.0); + b.add(g, g2h, g2p); + let (s3h, s3p) = shape(50.0, 50.0); + let s3 = b.add(f, s3h, s3p); + let doc = b.build(); + let r = run(&doc); + let _ = s0; + // group union = 70×30 → next sibling at 50+10+70+10 + assert_close(r.box_of(s3).x, 140.0, "sibling after group slot"); + assert_close(r.box_of(g).w, 70.0, "derived extent"); +} + +/// D-E1 (declared): empty group → zero bounds, resolves, renders nothing. +#[test] +fn de1_empty_group_zero_bounds() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(10.0); + let g = b.add(0, gh, Payload::Group); + let doc = b.build(); + let r = run(&doc); + assert_rect(r.box_of(g), 10.0, 0.0, 0.0, 0.0, "empty union at placement"); +} + +/// Lens: layout-transparent quarantine — ops affect paint (world of +/// children), never the box the parent lays out (a.md §3.3). +#[test] +fn lens_ops_are_layout_transparent() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(200.0), + Direction::Row, + 10.0, + 0.0, + ); + let f = b.add(0, h, p); + let (s0h, s0p) = shape(50.0, 50.0); + b.add(f, s0h, s0p); + let lens = b.add( + f, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Skew { + x_deg: 20.0, + y_deg: 0.0, + }], + }, + ); + let (ch, cp) = shape(240.0, 150.0); + let c = b.add(lens, ch, cp); + let (s3h, s3p) = shape(50.0, 50.0); + let s3 = b.add(f, s3h, s3p); + let doc = b.build(); + let r = run(&doc); + + // parent flex sees the pre-ops box (240 wide), not the skewed envelope + assert_close(r.box_of(lens).w, 240.0, "pre-ops box participates"); + assert_close(r.box_of(s3).x, 50.0 + 10.0 + 240.0 + 10.0, "sibling slot"); + // …but the child's world transform carries the skew + let w = r.world_of(c); + assert!( + (w.c - (20.0f32).to_radians().tan()).abs() < 1e-4, + "skew in world" + ); + // and the render bounds (world AABB) are wider than the box + assert!(r.aabb_of(lens).w > 240.0, "post-ops render bounds"); +} + +/// Both E1 modes agree on everything outside flow rotation (guard: the flag +/// touches exactly one semantic). +#[test] +fn flag_scope_is_limited_to_flow_rotation() { + let (doc, g, s1, s2) = rotated_group_doc(); + let ra = resolve(&doc, &opts()); + let rv = resolve(&doc, &opts_visual()); + for id in [g, s1, s2] { + assert_eq!(ra.world_of(id), rv.world_of(id), "free tree identical"); + } +} diff --git a/crates/n0-model/tests/draft0_resolution.rs b/crates/n0-model/tests/draft0_resolution.rs new file mode 100644 index 00000000..65801aaf --- /dev/null +++ b/crates/n0-model/tests/draft0_resolution.rs @@ -0,0 +1,430 @@ +//! Focused Draft 0 resolution regressions: text measurement order, flex +//! stretch intent, and stroke-aware visual bounds. + +mod common; +use common::*; + +use n0_model::math::RectF; +use n0_model::model::*; + +fn text_node( + width: SizeIntent, + min_width: Option, + max_width: Option, +) -> (Document, NodeId) { + let mut builder = DocBuilder::new(); + let mut header = Header::new(width, SizeIntent::Auto); + header.min_width = min_width; + header.max_width = max_width; + let text = builder.add( + 0, + header, + Payload::Text { + content: "aa bb cc".into(), + font_size: 10.0, + }, + ); + (builder.build(), text) +} + +#[test] +fn text_auto_height_remeasures_at_the_final_constrained_width() { + let cases = [ + (SizeIntent::Auto, None, Some(30.0), 30.0, 24.0), + (SizeIntent::Auto, Some(60.0), None, 60.0, 12.0), + (SizeIntent::Fixed(60.0), None, Some(30.0), 30.0, 24.0), + (SizeIntent::Fixed(30.0), Some(60.0), None, 60.0, 12.0), + ]; + + for (width, min_width, max_width, expected_width, expected_height) in cases { + let (doc, text) = text_node(width, min_width, max_width); + let resolved = run(&doc); + assert_close(resolved.box_of(text).w, expected_width, "final text width"); + assert_close( + resolved.box_of(text).h, + expected_height, + "height measured from final width", + ); + } +} + +#[test] +fn explicit_line_breaks_contribute_to_auto_text_extents() { + let mut builder = DocBuilder::new(); + let text = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Text { + content: "a\nbb\n".into(), + font_size: 10.0, + }, + ); + let resolved = run(&builder.build()); + assert_close(resolved.box_of(text).w, 12.0, "widest explicit line"); + assert_close( + resolved.box_of(text).h, + 36.0, + "trailing empty line is preserved", + ); +} + +fn column_flex(cross_align: CrossAlign) -> Payload { + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + direction: Direction::Column, + cross_align, + ..Default::default() + }, + clips_content: false, + } +} + +#[test] +fn container_cross_stretch_reads_authored_auto_for_ordinary_children() { + let mut builder = DocBuilder::new(); + let container = builder.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + column_flex(CrossAlign::Stretch), + ); + let auto = builder.add( + container, + Header::new(SizeIntent::Auto, SizeIntent::Fixed(20.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let fixed = builder.add( + container, + Header::new(SizeIntent::Fixed(30.0), SizeIntent::Fixed(20.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + + let resolved = run(&builder.build()); + assert_close(resolved.box_of(auto).w, 100.0, "auto cross size stretches"); + assert_close( + resolved.box_of(fixed).w, + 30.0, + "container stretch preserves fixed cross size", + ); +} + +#[test] +fn child_align_stretch_overrides_fixed_text_cross_size() { + let mut builder = DocBuilder::new(); + let container = builder.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + column_flex(CrossAlign::Start), + ); + let mut header = Header::new(SizeIntent::Fixed(30.0), SizeIntent::Auto); + header.self_align = SelfAlign::Stretch; + let text = builder.add( + container, + header, + Payload::Text { + content: "aaaa bbbb cccc".into(), + font_size: 10.0, + }, + ); + + let resolved = run(&builder.build()); + assert_close(resolved.box_of(text).w, 100.0, "explicit text stretch"); + assert_close( + resolved.box_of(text).h, + 12.0, + "text remeasures at stretched width", + ); +} + +#[test] +fn row_flex_self_stretch_preserves_the_lines_locked_zero_height() { + let mut builder = DocBuilder::new(); + let container = builder.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(80.0)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + direction: Direction::Row, + ..Default::default() + }, + clips_content: false, + }, + ); + let payload = Payload::Shape { + desc: ShapeDesc::Line, + }; + let mut header = Header::new(SizeIntent::Fixed(20.0), SizeIntent::Fixed(0.0)); + header.self_align = SelfAlign::Stretch; + let line = builder.add(container, header, payload.clone()); + let mut stroke = Stroke::default_for(&payload).unwrap(); + stroke.width = StrokeWidth::Uniform(10.0); + stroke.paints = Paints::solid(Color::BLACK); + builder.node_mut(line).strokes.push(stroke); + + let resolved = run(&builder.build()); + assert_close( + resolved.box_of(line).h, + 0.0, + "flex cannot rewrite locked line height", + ); + assert_close( + resolved.aabb_of(line).h, + 10.0, + "the line keeps a degenerate paint box plus stroke coverage", + ); +} + +fn stroked_shape(desc: ShapeDesc, align: StrokeAlign, width: f32) -> (Document, NodeId) { + let mut builder = DocBuilder::new(); + let mut header = Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(50.0)); + header.x = AxisBinding::start(10.0); + header.y = AxisBinding::start(20.0); + if desc == ShapeDesc::Line { + header.height = SizeIntent::Fixed(0.0); + } + let payload = Payload::Shape { desc }; + let mut stroke = Stroke::default_for(&payload).expect("shape supports strokes"); + stroke.paints = Paints::solid(Color::BLACK); + stroke.width = StrokeWidth::Uniform(width); + stroke.align = align; + let shape = builder.add(0, header, payload); + builder.node_mut(shape).strokes.push(stroke); + (builder.build(), shape) +} + +#[test] +fn effective_strokes_expand_visual_bounds_without_changing_layout_bounds() { + let cases = [ + (StrokeAlign::Inside, (10.0, 20.0, 100.0, 50.0)), + (StrokeAlign::Center, (5.0, 15.0, 110.0, 60.0)), + (StrokeAlign::Outside, (0.0, 10.0, 120.0, 70.0)), + ]; + + for (align, (x, y, width, height)) in cases { + let (doc, shape) = stroked_shape(ShapeDesc::Rect, align, 10.0); + let resolved = run(&doc); + assert_rect( + resolved.box_of(shape), + 10.0, + 20.0, + 100.0, + 50.0, + "stroke does not affect layout box", + ); + assert_rect( + resolved.aabb_of(shape), + x, + y, + width, + height, + "stroke expands visual bounds", + ); + } +} + +#[test] +fn per_side_strokes_expand_each_visual_bound_independently() { + let (mut doc, shape) = stroked_shape(ShapeDesc::Rect, StrokeAlign::Outside, 1.0); + doc.get_mut(shape).strokes[0].width = StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 2.0, + stroke_right_width: 4.0, + stroke_bottom_width: 6.0, + stroke_left_width: 8.0, + }); + + let resolved = run(&doc); + assert_rect( + resolved.box_of(shape), + 10.0, + 20.0, + 100.0, + 50.0, + "per-side stroke does not affect layout box", + ); + assert_rect( + resolved.aabb_of(shape), + 2.0, + 18.0, + 112.0, + 58.0, + "per-side stroke contributes asymmetric visual outsets", + ); +} + +#[test] +fn unsupported_rectangular_stroke_states_do_not_expand_visual_bounds() { + let cases = [ShapeDesc::Ellipse, ShapeDesc::Rect]; + for desc in cases { + let (mut doc, shape) = stroked_shape(desc.clone(), StrokeAlign::Outside, 1.0); + doc.get_mut(shape).strokes[0].width = StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 2.0, + stroke_right_width: 4.0, + stroke_bottom_width: 6.0, + stroke_left_width: 8.0, + }); + if desc == ShapeDesc::Rect { + doc.get_mut(shape).corner_smoothing = CornerSmoothing(0.5); + } + + let resolved = run(&doc); + assert_rect( + resolved.aabb_of(shape), + 10.0, + 20.0, + 100.0, + 50.0, + "unsupported rectangular stroke state has no visual coverage", + ); + } +} + +#[test] +fn stroke_expansion_is_transformed_with_the_node() { + let (mut doc, shape) = stroked_shape(ShapeDesc::Rect, StrokeAlign::Outside, 10.0); + doc.get_mut(shape).header.rotation = 90.0; + let resolved = run(&doc); + assert_rect( + resolved.aabb_of(shape), + 25.0, + -15.0, + 70.0, + 120.0, + "expanded local stroke bounds rotate into world bounds", + ); +} + +#[test] +fn line_stroke_bounds_expand_conservatively_around_the_degenerate_axis() { + let (doc, line) = stroked_shape(ShapeDesc::Line, StrokeAlign::Center, 10.0); + let resolved = run(&doc); + assert_rect( + resolved.box_of(line), + 10.0, + 20.0, + 100.0, + 0.0, + "line layout box remains degenerate", + ); + assert_rect( + resolved.aabb_of(line), + 5.0, + 15.0, + 110.0, + 10.0, + "line caps and width fit conservative bounds", + ); +} + +#[test] +fn fontless_stub_text_does_not_invent_glyph_or_stroke_ink() { + let mut builder = DocBuilder::new(); + let mut header = Header::new(SizeIntent::Auto, SizeIntent::Auto); + header.x = AxisBinding::start(20.0); + header.y = AxisBinding::start(20.0); + let payload = Payload::Text { + content: "A".into(), + font_size: 10.0, + }; + let text = builder.add(0, header, payload.clone()); + let mut stroke = Stroke::default_for(&payload).unwrap(); + stroke.paints = Paints::solid(Color::BLACK); + stroke.width = StrokeWidth::Uniform(2.0); + stroke.align = StrokeAlign::Center; + builder.node_mut(text).strokes.push(stroke); + + let resolved = run(&builder.build()); + assert_rect( + resolved.aabb_of(text), + 20.0, + 20.0, + 0.0, + 0.0, + "a glyphless layout has no outline for its text stroke to cover", + ); +} + +#[test] +fn ineffective_strokes_do_not_expand_visual_bounds() { + let (mut doc, shape) = stroked_shape(ShapeDesc::Rect, StrokeAlign::Outside, 10.0); + let stroke = &mut doc.get_mut(shape).strokes[0]; + stroke.paints = Paints::solid(Color::TRANSPARENT); + let resolved = run(&doc); + assert_rect( + resolved.aabb_of(shape), + 10.0, + 20.0, + 100.0, + 50.0, + "transparent stroke has no visual extent", + ); + + doc.get_mut(shape).strokes[0].paints = Paints::solid(Color::BLACK); + doc.get_mut(shape).strokes[0].width = StrokeWidth::None; + let resolved = run(&doc); + assert_rect( + resolved.aabb_of(shape), + 10.0, + 20.0, + 100.0, + 50.0, + "zero-width stroke has no visual extent", + ); +} + +#[test] +fn transformed_clip_bounds_exclude_outside_descendant_contributions() { + let mut builder = DocBuilder::new(); + let mut clip_header = Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(60.0)); + clip_header.x = AxisBinding::start(200.0); + clip_header.y = AxisBinding::start(100.0); + clip_header.rotation = 90.0; + let clip = builder.add( + 0, + clip_header, + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: true, + }, + ); + let mut child_header = Header::new(SizeIntent::Fixed(20.0), SizeIntent::Fixed(20.0)); + child_header.x = AxisBinding::start(150.0); + child_header.y = AxisBinding::start(10.0); + let child = builder.add( + clip, + child_header, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + + let resolved = run(&builder.build()); + let clip_world_aabb = RectF { + x: 0.0, + y: 0.0, + w: 100.0, + h: 60.0, + } + .transformed_aabb(&resolved.world_of(clip)); + assert_eq!( + resolved.aabb_of(clip), + clip_world_aabb, + "a clipped-out child cannot enlarge its ancestor's visual bounds" + ); + assert_eq!( + resolved.aabb_of(child), + RectF { + x: 250.0, + y: 230.0, + w: 20.0, + h: 20.0, + }, + "the child's own conservative AABB remains independently readable" + ); +} diff --git a/crates/n0-model/tests/edge_census.rs b/crates/n0-model/tests/edge_census.rs new file mode 100644 index 00000000..b4acf1ad --- /dev/null +++ b/crates/n0-model/tests/edge_census.rs @@ -0,0 +1,578 @@ +//! Edge census — the ship-readiness sweep across implemented kinds +//! (text, group, lens, rotation extremes, degenerate boxes). +//! Every test either proves an edge or would have become a finding. + +mod common; +use common::*; + +use n0_model::math::Affine; +use n0_model::model::*; + +// ---------- text edges ---------- + +/// Empty text: zero width, one line-height tall — never a NaN, never 0×0. +#[test] +fn text_empty_string() { + let mut b = DocBuilder::new(); + let t = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Text { + content: String::new(), + font_size: 16.0, + }, + ); + let r = run(&b.build()); + let bx = r.box_of(t); + assert_close(bx.w, 0.0, "empty text w"); + assert_close(bx.h, 19.2, "empty text keeps line height"); +} + +/// A single word wider than the fixed width: no mid-word break — the box +/// keeps the fixed width, content overflows in paint (declared). +#[test] +fn text_giant_word_no_break() { + let mut b = DocBuilder::new(); + let t = b.add( + 0, + Header::new(SizeIntent::Fixed(30.0), SizeIntent::Auto), + Payload::Text { + content: "unbreakable".into(), // 11 chars × 6 = 66 > 30 + font_size: 10.0, + }, + ); + let r = run(&b.build()); + let bx = r.box_of(t); + assert_close(bx.w, 30.0, "fixed width kept"); + assert_close(bx.h, 12.0, "single line despite overflow"); +} + +/// Fixed height smaller than content: the box is the intent; overflow is +/// a paint concern (T-E2 positioning math is renderer work). +#[test] +fn text_fixed_height_clips_not_grows() { + let mut b = DocBuilder::new(); + let t = b.add( + 0, + Header::new(SizeIntent::Fixed(36.0), SizeIntent::Fixed(10.0)), + Payload::Text { + content: "hello world".into(), // wraps to 2 lines at 36 + font_size: 10.0, + }, + ); + let r = run(&b.build()); + assert_close(r.box_of(t).h, 10.0, "declared height wins the box"); +} + +/// Text as the only child of a 3-deep hug chain: measurement propagates +/// bottom-up through every hug without a cycle. +#[test] +fn text_hug_chain_3_deep() { + let mut b = DocBuilder::new(); + let mut parent = 0; + let mut frames = vec![]; + for _ in 0..3 { + let (h, p) = frame_flex(SizeIntent::Auto, SizeIntent::Auto, Direction::Row, 0.0, 5.0); + parent = b.add(parent, h, p); + frames.push(parent); + } + let t = b.add( + parent, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Text { + content: "hi".into(), + font_size: 10.0, + }, + ); + let r = run(&b.build()); + assert_close(r.box_of(t).w, 12.0, "text natural"); + // each hug adds 10 (padding 5×2) + assert_close(r.box_of(frames[2]).w, 22.0, "inner hug"); + assert_close(r.box_of(frames[1]).w, 32.0, "middle hug"); + assert_close(r.box_of(frames[0]).w, 42.0, "outer hug"); +} + +/// min clamps beat the measured size (measured kind × constraint). +#[test] +fn text_min_width_beats_measured() { + let mut b = DocBuilder::new(); + let mut h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + h.min_width = Some(100.0); + let t = b.add( + 0, + h, + Payload::Text { + content: "hi".into(), // natural 12 + font_size: 10.0, + }, + ); + let r = run(&b.build()); + assert_close(r.box_of(t).w, 100.0, "min wins"); +} + +// ---------- group edges ---------- + +/// Empty group in flex: zero-extent slot; siblings separated by gaps only. +#[test] +fn empty_group_in_flex() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(100.0), + Direction::Row, + 10.0, + 10.0, + ); + let f = b.add(0, h, p); + let (h1, p1) = shape(50.0, 50.0); + b.add(f, h1, p1); + b.add( + f, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (h2, p2) = shape(50.0, 50.0); + let c = b.add(f, h2, p2); + let r = run(&b.build()); + // 10 + 50 + 10 + 0 + 10 = 80 + assert_close(r.box_of(c).x, 80.0, "zero slot + both gaps"); +} + +/// Group whose only child is hidden ≡ empty group (union skips inactive). +#[test] +fn group_of_hidden_children_is_empty() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(40.0); + let g = b.add(0, gh, Payload::Group); + let (mut h1, p1) = shape(50.0, 50.0); + h1.active = false; + b.add(g, h1, p1); + let r = run(&b.build()); + assert_rect(r.box_of(g), 40.0, 0.0, 0.0, 0.0, "empty union at origin"); +} + +/// Nested groups 3 deep, each rotated: composition stays rigid and +/// editing the innermost child never moves the outer sibling (D-E2). +#[test] +fn nested_groups_3_deep_rotated() { + let build = |inner_x: f32| { + let mut b = DocBuilder::new(); + let mut g1h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + g1h.x = AxisBinding::start(100.0); + g1h.rotation = 10.0; + let g1 = b.add(0, g1h, Payload::Group); + let (sh, sp) = shape(30.0, 30.0); // outer sibling + let s = b.add(g1, sh, sp); + let mut g2h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + g2h.x = AxisBinding::start(50.0); + g2h.rotation = 15.0; + let g2 = b.add(g1, g2h, Payload::Group); + let mut g3h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + g3h.rotation = 20.0; + let g3 = b.add(g2, g3h, Payload::Group); + let (mut ch, cp) = shape(20.0, 20.0); + ch.x = AxisBinding::start(inner_x); + let c = b.add(g3, ch, cp); + (b.build(), s, c) + }; + let (d1, s1, c1) = build(0.0); + let (d2, s2, _) = build(-25.0); // move the innermost child + let r1 = run(&d1); + let r2 = run(&d2); + // outer sibling world-stable under inner edit (origin placement at depth) + let p1 = r1.world_of(s1).apply((0.0, 0.0)); + let p2 = r2.world_of(s2).apply((0.0, 0.0)); + assert_close(p1.0, p2.0, "sibling stable x at depth"); + assert_close(p1.1, p2.1, "sibling stable y at depth"); + // and rotations compose (10+15+20 = 45 on the innermost child) + let w = r1.world_of(c1); + let ang = w.b.atan2(w.a).to_degrees(); + assert_close(ang, 45.0, "rotations compose at depth"); +} + +/// Union of rotated members uses oriented corners at every level. +#[test] +fn group_union_of_rotated_members() { + let mut b = DocBuilder::new(); + let g = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (mut h1, p1) = shape(40.0, 40.0); + h1.rotation = 45.0; // envelope 56.57 about center (20,20) + b.add(g, h1, p1); + let r = run(&b.build()); + let u = r.box_of(g); + let d = 40.0 * (2.0f32).sqrt(); + assert_close(u.w, d, "oriented union w"); + assert_close(u.h, d, "oriented union h"); +} + +// ---------- lens edges ---------- + +/// Lens-in-lens: ops compose outer∘inner (nesting = composition, declared). +#[test] +fn lens_in_lens_composes() { + let mut b = DocBuilder::new(); + let l1 = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Translate { x: 10.0, y: 0.0 }], + }, + ); + let l2 = b.add( + l1, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Translate { x: 0.0, y: 5.0 }], + }, + ); + let (sh, sp) = shape(20.0, 20.0); + let s = b.add(l2, sh, sp); + let r = run(&b.build()); + let p = r.world_of(s).apply((0.0, 0.0)); + assert_close(p.0, 10.0, "outer op applied"); + assert_close(p.1, 5.0, "inner op applied"); +} + +/// Ordered ops: rotate∘scale ≠ scale∘rotate — the list order is honored. +#[test] +fn lens_ops_order_matters() { + let world_of = |ops: Vec| { + let mut b = DocBuilder::new(); + let l = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { ops }, + ); + let (sh, sp) = shape(20.0, 10.0); + let s = b.add(l, sh, sp); + let r = run(&b.build()); + r.world_of(s) + }; + let a = world_of(vec![ + LensOp::Rotate { deg: 90.0 }, + LensOp::Scale { x: 2.0, y: 1.0 }, + ]); + let bm = world_of(vec![ + LensOp::Scale { x: 2.0, y: 1.0 }, + LensOp::Rotate { deg: 90.0 }, + ]); + assert!( + (a.a - bm.a).abs() > 1e-3 || (a.b - bm.b).abs() > 1e-3 || (a.e - bm.e).abs() > 1e-3, + "op order is semantic" + ); +} + +/// Matrix op with negative determinant: a flip smuggled through the +/// quarantine renders as given (the lens is the anything-goes tier). +#[test] +fn lens_matrix_negative_determinant() { + let mut b = DocBuilder::new(); + let l = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Matrix { + m: [-1.0, 0.0, 0.0, 1.0, 40.0, 0.0], // mirror-x then shift + }], + }, + ); + let (sh, sp) = shape(20.0, 10.0); + let s = b.add(l, sh, sp); + let r = run(&b.build()); + let w = r.world_of(s); + let det = w.a * w.d - w.b * w.c; + assert!(det < 0.0, "negative determinant preserved"); +} + +/// Scale(0) collapses paint but never poisons layout: the pre-ops box +/// still participates; world AABB degenerates to a point, finitely. +#[test] +fn lens_scale_zero_degenerate() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(100.0), + Direction::Row, + 10.0, + 10.0, + ); + let f = b.add(0, h, p); + let l = b.add( + f, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Scale { x: 0.0, y: 0.0 }], + }, + ); + let (sh, sp) = shape(50.0, 50.0); + b.add(l, sh, sp); + let (h2, p2) = shape(50.0, 50.0); + let c = b.add(f, h2, p2); + let r = run(&b.build()); + assert_close(r.box_of(c).x, 70.0, "layout uses pre-ops box (10+50+10)"); + let aabb = r.aabb_of(l); + assert!( + aabb.w.is_finite() && aabb.w < 1e-3, + "paint collapsed, finite" + ); +} + +// ---------- rotation extremes ---------- + +/// Winding is authored intent: rotation=450 resolves like 90° but the +/// document keeps 450 (R-E2's stored-as-authored half). +#[test] +fn rotation_winding_stored_as_authored() { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(100.0, 50.0); + h.rotation = 450.0; + let s = b.add(0, h, p); + let doc = b.build(); + let r = run(&doc); + let w90 = Affine::rotate_deg(90.0); + let w = r.world_of(s); + assert_close(w.a, w90.a, "resolves as 90°"); + assert_close(w.b, w90.b, "resolves as 90°"); + assert_eq!(doc.get(s).header.rotation, 450.0, "document keeps 450"); +} + +/// Full-bleed Span child rotated: the box stays edge-bound; the corners +/// swing outside the parent (declared: clipping is paint, never geometry). +#[test] +fn span_full_bleed_rotated_overflows() { + let mut b = DocBuilder::new(); + let (mut fh, fp) = frame_free(SizeIntent::Fixed(400.0), SizeIntent::Fixed(300.0)); + fh.x = AxisBinding::start(0.0); + let f = b.add(0, fh, fp); + let mut h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + h.x = AxisBinding::Span { + start: 0.0, + end: 0.0, + }; + h.y = AxisBinding::Span { + start: 0.0, + end: 0.0, + }; + h.rotation = 30.0; + let s = b.add( + f, + h, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let r = run(&b.build()); + assert_rect( + r.box_of(s), + 0.0, + 0.0, + 400.0, + 300.0, + "span box unbent by rotation", + ); + let aabb = r.aabb_of(s); + assert!(aabb.w > 400.0 && aabb.h > 300.0, "corners swing outside"); +} + +/// Accumulation: 120 × 3° successive writes vs one 360° — same resolved +/// geometry within the platform tolerance (R-E4). +#[test] +fn rotation_accumulation_bounded() { + let mut b = DocBuilder::new(); + let (h, p) = shape(100.0, 50.0); + let s = b.add(0, h, p); + let mut doc = b.build(); + for i in 1..=120 { + n0_model::ops::set_rotation(&mut doc, s, i as f32 * 3.0).unwrap(); + } + let r = run(&doc); + let w = r.world_of(s); + // 360°: bit-clean identity rotation per R-E1 quadrant rule + assert_close(w.a, 1.0, "a"); + assert_close(w.b, 0.0, "b"); + // set-rotation is absolute, not incremental — no accumulation by design + assert_eq!(doc.get(s).header.rotation, 360.0); +} + +/// aspect_ratio resolves the axis, THEN rotation wraps the result — +/// the two features compose without ordering ambiguity. +#[test] +fn aspect_ratio_then_rotation() { + let mut b = DocBuilder::new(); + let mut h = Header::new(SizeIntent::Fixed(160.0), SizeIntent::Auto); + h.aspect_ratio = Some((16.0, 9.0)); + h.rotation = 90.0; + let s = b.add( + 0, + h, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let r = run(&b.build()); + let bx = r.box_of(s); + assert_close(bx.w, 160.0, "aspect resolved w"); + assert_close(bx.h, 90.0, "aspect resolved h"); + let aabb = r.aabb_of(s); + assert_close(aabb.w, 90.0, "then envelope swaps"); + assert_close(aabb.h, 160.0, "then envelope swaps"); +} + +/// Far-canvas: rotation at 1e6 offsets keeps the center exact within +/// f32 tolerance at that scale (N-1 smoke). +#[test] +fn far_canvas_rotation_center() { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::start(1.0e6); + h.y = AxisBinding::start(1.0e6); + h.rotation = 33.0; + let s = b.add(0, h, p); + let r = run(&b.build()); + let c = r.world_of(s).apply((60.0, 40.0)); + // f32 ULP at 1e6 is 0.0625 — allow a scaled tolerance + assert!( + (c.0 - 1_000_060.0).abs() < 0.25, + "center x at far canvas: {}", + c.0 + ); + assert!( + (c.1 - 1_000_040.0).abs() < 0.25, + "center y at far canvas: {}", + c.1 + ); +} + +/// Census blocker fix: Span{0,0} (the canonical free-context fill) is a +/// wrap constraint — fill text re-wraps like Fixed/stretched widths. +#[test] +fn span_fill_text_rewraps() { + let mut b = DocBuilder::new(); + let (mut fh, fp) = frame_free(SizeIntent::Fixed(100.0), SizeIntent::Fixed(200.0)); + fh.x = AxisBinding::start(0.0); + let f = b.add(0, fh, fp); + let mut th = Header::new(SizeIntent::Auto, SizeIntent::Auto); + th.x = AxisBinding::Span { + start: 0.0, + end: 0.0, + }; + let t = b.add( + f, + th, + Payload::Text { + content: "aaaa bbbb cccc dddd".into(), // natural 114 @fs10 + font_size: 10.0, + }, + ); + let r = run(&b.build()); + let bx = r.box_of(t); + assert_close(bx.w, 100.0, "span fill width"); + // wraps at 100 (16 chars max): "aaaa bbbb cccc"(14) / "dddd" → 2 lines + assert_close(bx.h, 24.0, "fill text re-wraps"); +} + +/// Census fix: nested derived unions keep the inner union offset — a +/// group-in-group's content lands where the coordinates say (D-E2 depth). +#[test] +fn nested_group_union_offset_exact() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(100.0); + gh.y = AxisBinding::start(50.0); + let g = b.add(0, gh, Payload::Group); + let h = b.add( + g, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (mut sh, sp) = shape(40.0, 40.0); + sh.x = AxisBinding::start(20.0); + let s = b.add(h, sh, sp); + let r = run(&b.build()); + let p = r.world_of(s).apply((0.0, 0.0)); + assert_close(p.0, 120.0, "inner offset survives nesting"); + assert_close(p.1, 50.0, "y exact"); +} + +/// Census fix: ungroup with a NESTED group child preserves world +/// transforms (D-4 for derived children — origin bake, not center bake). +#[test] +fn ungroup_nested_group_preserves_world() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(100.0); + gh.y = AxisBinding::start(50.0); + gh.rotation = 30.0; + let g = b.add(0, gh, Payload::Group); + let h = b.add( + g, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (mut s1h, s1p) = shape(40.0, 40.0); + s1h.x = AxisBinding::start(20.0); + let s1 = b.add(h, s1h, s1p); + let (mut s2h, s2p) = shape(40.0, 40.0); + s2h.x = AxisBinding::start(80.0); + let s2 = b.add(h, s2h, s2p); + let mut doc = b.build(); + let before = run(&doc); + let w1 = before.world_of(s1); + let w2 = before.world_of(s2); + n0_model::ops::ungroup(&mut doc, &before, g).unwrap(); + let after = run(&doc); + for (id, w) in [(s1, w1), (s2, w2)] { + let v = after.world_of(id); + for (a, e) in [ + (v.a, w.a), + (v.b, w.b), + (v.c, w.c), + (v.d, w.d), + (v.e, w.e), + (v.f, w.f), + ] { + assert!((a - e).abs() < 1e-3, "nested bake exact: {a} vs {e}"); + } + } +} + +/// Census fix: a hug frame wraps a rotated GROUP by its true origin-pivot +/// envelope, not a center-concentric approximation. +#[test] +fn hug_wraps_rotated_group_exactly() { + let mut b = DocBuilder::new(); + let (fh, fp) = frame_free(SizeIntent::Auto, SizeIntent::Auto); + let f = b.add(0, fh, fp); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.rotation = 90.0; + let g = b.add(f, gh, Payload::Group); + let (sh, sp) = shape(100.0, 20.0); + b.add(g, sh, sp); + let doc = b.build(); + let r = run(&doc); + // origin pivot at (0,0): the 100×20 bar rotates into x∈[−20,0], y∈[0,100] + // → hug covers max extents (clamped at 0 origin): w = 0, h = 100… + // declared: hug covers positive extents; the frame is 0-wide? No — + // the AABB max_x = 0 → w=0 is degenerate but honest; assert h. + assert_close(r.box_of(f).h, 100.0, "hug height = true envelope"); + assert!(r.box_of(f).w < 1.0, "no phantom width from the wrong pivot"); +} + +/// R-E3: −0.0 canonicalizes at the write boundary. +#[test] +fn negative_zero_rotation_canonicalized() { + let mut b = DocBuilder::new(); + let (h, p) = shape(50.0, 50.0); + let s = b.add(0, h, p); + let mut doc = b.build(); + n0_model::ops::set_rotation(&mut doc, s, -0.0).unwrap(); + assert!( + doc.get(s).header.rotation.is_sign_positive(), + "−0.0 never stored" + ); +} diff --git a/crates/n0-model/tests/escape.rs b/crates/n0-model/tests/escape.rs new file mode 100644 index 00000000..b8755f9b --- /dev/null +++ b/crates/n0-model/tests/escape.rs @@ -0,0 +1,128 @@ +//! The escape hatches from layout-visible rotation (owner question, +//! 2026-07-07): can a user opt OUT of "AABB participates" per node? +//! +//! Three structural escapes, no mode flag: +//! 1. lens-rotate → paint-only rotation; slot = unrotated box +//! (the CSS-semantics twin, exactly) +//! 2. flow="absolute" → out of flow entirely +//! 3. wrapper frame → custom slot reservation (incl. the +//! diagonal-stable wrapper: zero breathing at any θ) + +mod common; +use common::*; + +use n0_model::model::*; +use n0_model::resolve::resolve; + +fn flex_row_of_cards() -> DocBuilder { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(140.0), + Direction::Row, + 10.0, + 10.0, + ); + b.add(0, h, p); + b +} + +/// Escape 1: a lens with a Rotate op reproduces the visual-only arm +/// EXACTLY — unrotated box holds the slot, rotation is paint-only. +#[test] +fn lens_rotate_is_the_visual_only_twin() { + // Document A: header rotation, resolved under the CSS control flag. + let mut ba = flex_row_of_cards(); + let f_a = 1; + let mut cards_a = vec![]; + for _ in 0..3 { + let (h, p) = shape(60.0, 100.0); + cards_a.push(ba.add(f_a, h, p)); + } + let mut doc_a = ba.build(); + doc_a.get_mut(cards_a[1]).header.rotation = 90.0; + let ra = resolve(&doc_a, &opts_visual()); + + // Document B: lens-wrapped middle card, resolved under the LOCKED + // default (AABB participates). The lens is layout-transparent, so + // its pre-ops (unrotated) box takes the slot. + let mut bb = flex_row_of_cards(); + let f_b = 1; + let (h0, p0) = shape(60.0, 100.0); + let c0 = bb.add(f_b, h0, p0); + let lens = bb.add( + f_b, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Rotate { deg: 90.0 }], + }, + ); + let (h1, p1) = shape(60.0, 100.0); + let inner = bb.add(lens, h1, p1); + let (h2, p2) = shape(60.0, 100.0); + let c2 = bb.add(f_b, h2, p2); + let doc_b = bb.build(); + let rb = resolve(&doc_b, &opts()); // default: AabbParticipates + + // identical layout: container width, sibling slots + assert_close(ra.box_of(f_a).w, 220.0, "control container"); + assert_close(rb.box_of(f_b).w, 220.0, "lens escape container"); + assert_close( + ra.box_of(cards_a[2]).x, + rb.box_of(c2).x, + "third card position identical", + ); + // identical paint: the rotated card's world AABB matches + let aabb_a = ra.aabb_of(cards_a[1]); + let aabb_b = rb.aabb_of(inner); + assert_close(aabb_a.w, aabb_b.w, "painted envelope w"); + assert_close(aabb_a.h, aabb_b.h, "painted envelope h"); + assert_close(aabb_a.x, aabb_b.x, "painted envelope x"); + // and the overlap the user asked for is present in both + let ov = rb.aabb_of(c0).intersection_area(&rb.aabb_of(inner)); + assert_close(ov, 600.0, "paint-only overlap, opted into deliberately"); +} + +/// Escape 3: a fixed wrapper frame owns the slot; the rotated child +/// paints beyond it. Sized to the diagonal, the slot is θ-stable — +/// zero container breathing at ANY angle (the animation-friendly form). +#[test] +fn diagonal_wrapper_gives_theta_stable_slot() { + let diag = (60.0f32 * 60.0 + 100.0 * 100.0).sqrt(); // 116.62 + let mut widths = vec![]; + for theta in [0.0f32, 30.0, 59.0, 90.0, 137.0] { + let mut b = flex_row_of_cards(); + let f = 1; + let (h0, p0) = shape(60.0, 100.0); + b.add(f, h0, p0); + // wrapper: fixed diagonal-sized free frame, no layout, no clip + let wrap = b.add( + f, + Header::new(SizeIntent::Fixed(diag), SizeIntent::Fixed(diag)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let (mut hc, pc) = shape(60.0, 100.0); + hc.x = AxisBinding::center(0.0); + hc.y = AxisBinding::center(0.0); + hc.rotation = theta; + b.add(wrap, hc, pc); + let (h2, p2) = shape(60.0, 100.0); + b.add(f, h2, p2); + let doc = b.build(); + let r = resolve(&doc, &opts()); + widths.push(r.box_of(f).w); + } + // container width identical at every angle: the wrapper absorbed + // the envelope; nothing breathes, nothing overlaps siblings + for w in &widths { + assert_close(*w, widths[0], "θ-stable container"); + } + assert_close( + widths[0], + 10.0 + 60.0 + 10.0 + 116.61903 + 10.0 + 60.0 + 10.0, + "diagonal slot", + ); +} diff --git a/crates/n0-model/tests/flip.rs b/crates/n0-model/tests/flip.rs new file mode 100644 index 00000000..9b6951fb --- /dev/null +++ b/crates/n0-model/tests/flip.rs @@ -0,0 +1,354 @@ +//! Flip conformance — E-A2 semantics + B1 pivot rule + the cross-zero +//! resize gesture (owner-raised red flag, 2026-07-07). +//! +//! Laws under test: +//! - F-1: flip on boxed kinds is center-applied → AABB-invariant → +//! layout-invisible (the one transform that never pops layout). +//! - F-2: flip composes innermost (T·R·F): a flipped node's local equals +//! the unflipped local pre-composed with the center mirror. +//! - F-3 (B1): flip pivot per kind follows rotation — origin for derived; +//! D-2 sibling stability holds under a flipped group. +//! - F-4: cross-zero drag re-targets (|extent| + flip toggle + re-pin), +//! tracks the gesture, and is an identity when dragged out and back. +//! - F-5: the typed write stays a wall — set_width(−x) = NegativeExtent, +//! document untouched (M-6). +//! - F-6: ungroup bakes flips exactly (σ conjugation): world transforms +//! of children are preserved. + +use n0_model::math::Affine; +use n0_model::model::*; +use n0_model::ops; +use n0_model::ops::{Axis, OpError, ResizeDrag}; +use n0_model::resolve::{resolve, ResolveOptions}; + +fn opts() -> ResolveOptions { + ResolveOptions { + viewport: (1000.0, 800.0), + ..Default::default() + } +} + +fn approx(a: f32, b: f32, eps: f32) -> bool { + (a - b).abs() <= eps +} + +fn affine_approx(a: &Affine, b: &Affine, eps: f32) -> bool { + approx(a.a, b.a, eps) + && approx(a.b, b.b, eps) + && approx(a.c, b.c, eps) + && approx(a.d, b.d, eps) + && approx(a.e, b.e, eps) + && approx(a.f, b.f, eps) +} + +fn card(w: f32, h: f32) -> (Header, Payload) { + ( + Header::new(SizeIntent::Fixed(w), SizeIntent::Fixed(h)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ) +} + +fn flex_row(w: f32, h: f32) -> (Header, Payload) { + ( + Header::new(SizeIntent::Fixed(w), SizeIntent::Fixed(h)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + gap_main: 10.0, + padding: EdgeInsets::all(10.0), + cross_align: CrossAlign::Center, + ..Default::default() + }, + clips_content: false, + }, + ) +} + +fn at(mut h: Header, x: f32, y: f32) -> Header { + h.x = AxisBinding::start(x); + h.y = AxisBinding::start(y); + h +} + +// --- F-1: layout invisibility ------------------------------------------ + +#[test] +fn f1_flip_is_layout_invisible_in_flow() { + let build = |flip: bool| { + let mut b = DocBuilder::new(); + let (fh, fp) = flex_row(460.0, 170.0); + let f = b.add(0, at(fh, 20.0, 20.0), fp); + let mut ids = vec![]; + for i in 0..3 { + let (mut ch, cp) = card(60.0, 100.0); + if i == 1 && flip { + ch.flip_x = true; + ch.flip_y = true; + } + ids.push(b.add(f, ch, cp)); + } + (b.build(), ids) + }; + let (d0, ids0) = build(false); + let (d1, ids1) = build(true); + let r0 = resolve(&d0, &opts()); + let r1 = resolve(&d1, &opts()); + for (a, b) in ids0.iter().zip(ids1.iter()) { + // Boxes identical: flip is center-applied, the AABB cannot change. + assert_eq!(r0.box_of(*a), r1.box_of(*b)); + let wa = r0.aabb_of(*a); + let wb = r1.aabb_of(*b); + assert!(approx(wa.x, wb.x, 1e-4) && approx(wa.w, wb.w, 1e-4)); + } + // ...and the flipped node's world really mirrors (det < 0 is only + // possible with an even/odd flip count — here x·y = det positive again, + // so check the axes swapped sign instead). + let w = r1.world_of(ids1[1]); + assert!(w.a < 0.0 && w.d < 0.0, "both axes mirrored: {w:?}"); +} + +// --- F-2: composition order -------------------------------------------- + +#[test] +fn f2_flip_composes_innermost_about_center() { + let (w, h, theta) = (100.0f32, 60.0f32, 30.0f32); + let base = Affine::from_box_center(10.0, 20.0, w, h, theta); + let flipped = Affine::from_box_center_flip(10.0, 20.0, w, h, theta, true, false); + // T·R·F_c means: flipped(p) == base(mirror_c(p)). + for p in [(0.0, 0.0), (w, 0.0), (30.0, 45.0), (w / 2.0, h / 2.0)] { + let mirrored = (w - p.0, p.1); + let lhs = flipped.apply(p); + let rhs = base.apply(mirrored); + assert!( + approx(lhs.0, rhs.0, 1e-3) && approx(lhs.1, rhs.1, 1e-3), + "p={p:?} lhs={lhs:?} rhs={rhs:?}" + ); + } + // Center is a fixed point of the mirror. + let c = flipped.apply((w / 2.0, h / 2.0)); + assert!(approx(c.0, 60.0, 1e-3) && approx(c.1, 50.0, 1e-3)); + // Single-axis mirror ⇒ negative determinant. + let det = flipped.a * flipped.d - flipped.b * flipped.c; + assert!(det < 0.0); +} + +// --- F-3: B1 pivot rule + D-2 under flip -------------------------------- + +#[test] +fn f3_d2_sibling_stability_under_flipped_group() { + let build = |ax: f32| { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(200.0); + gh.y = AxisBinding::start(100.0); + gh.flip_x = true; + gh.rotation = 15.0; + let g = b.add(0, gh, Payload::Group); + let (c1h, c1p) = card(50.0, 50.0); + let a = b.add(g, at(c1h, ax, 0.0), c1p); + let (c2h, c2p) = card(40.0, 40.0); + let s = b.add(g, at(c2h, 120.0, 80.0), c2p); + (b.build(), a, s) + }; + // Move child A (its stored offset changes); sibling S must not move in + // world space — origin pivot means the union never feeds back (E-A1), + // and B1 gives flip the same pivot. + let (d0, _a0, s0) = build(0.0); + let (d1, _a1, s1) = build(-60.0); + let r0 = resolve(&d0, &opts()); + let r1 = resolve(&d1, &opts()); + assert!( + affine_approx(&r0.world_of(s0), &r1.world_of(s1), 1e-4), + "sibling moved under flipped group: {:?} vs {:?}", + r0.world_of(s0), + r1.world_of(s1) + ); +} + +// --- F-4: the cross-zero gesture ---------------------------------------- + +/// Free frame + one rect; returns (doc, rect id). +fn free_rect() -> (Document, NodeId) { + let mut b = DocBuilder::new(); + let (ch, cp) = card(140.0, 80.0); + let id = b.add(0, at(ch, 60.0, 40.0), cp); + (b.build(), id) +} + +#[test] +fn f4_cross_zero_drag_flips_and_tracks_anchor() { + let (mut doc, id) = free_rect(); + let r = resolve(&doc, &opts()); + // Left edge fixed at 60; user drags the right handle. + let drag = ResizeDrag::begin(&doc, &r, id, Axis::X, AnchorEdge::Start).unwrap(); + assert_eq!(drag.anchor, 60.0); + + // Still on the base side: plain resize, no flip. + let n = ops::resize_drag(&mut doc, &r, id, &drag, 260.0).unwrap(); + assert_eq!(n, 2); // extent + position (x unchanged numerically but re-written) + let r = resolve(&doc, &opts()); + assert_eq!(r.xywh(id), (60.0, 40.0, 200.0, 80.0)); + assert!(!doc.get(id).header.flip_x); + + // Cross the anchor: the box mirrors and keeps tracking the hand. + let n = ops::resize_drag(&mut doc, &r, id, &drag, -20.0).unwrap(); + assert_eq!(n, 3); // extent + position + flip toggle + let r = resolve(&doc, &opts()); + assert_eq!(r.xywh(id), (-20.0, 40.0, 80.0, 80.0)); + assert!(doc.get(id).header.flip_x); + // The fixed edge is still at 60 — now the box's max edge. + let b = r.box_of(id); + assert_eq!(b.x + b.w, 60.0); + // Content is genuinely mirrored: negative determinant. + let w = r.world_of(id); + assert!(w.a * w.d - w.b * w.c < 0.0); +} + +#[test] +fn f4_cross_zero_round_trip_is_identity() { + let (mut doc, id) = free_rect(); + let original = doc.clone(); + let r0 = resolve(&doc, &opts()); + let drag = ResizeDrag::begin(&doc, &r0, id, Axis::X, AnchorEdge::Start).unwrap(); + + // Out past zero... + let r = resolve(&doc, &opts()); + ops::resize_drag(&mut doc, &r, id, &drag, -80.0).unwrap(); + // ...through a second move on the far side... + let r = resolve(&doc, &opts()); + ops::resize_drag(&mut doc, &r, id, &drag, -140.0).unwrap(); + // ...and back to the exact starting handle position (60 + 140). + let r = resolve(&doc, &opts()); + ops::resize_drag(&mut doc, &r, id, &drag, 200.0).unwrap(); + + assert_eq!(doc, original, "drag out and back must be the identity"); +} + +#[test] +fn f4_zero_width_moment_is_legal_and_unflipped() { + let (mut doc, id) = free_rect(); + let r = resolve(&doc, &opts()); + let drag = ResizeDrag::begin(&doc, &r, id, Axis::X, AnchorEdge::Start).unwrap(); + // Target exactly at the anchor: extent 0, no flip, no error (N-2 allows + // zero extents; the sign only appears strictly past the edge). + ops::resize_drag(&mut doc, &r, id, &drag, 60.0).unwrap(); + let r = resolve(&doc, &opts()); + assert_eq!(r.xywh(id).2, 0.0); + assert!(!doc.get(id).header.flip_x); +} + +#[test] +fn f4_end_edge_drag_crosses_negative_side() { + let (mut doc, id) = free_rect(); + let r = resolve(&doc, &opts()); + // Right edge fixed at 200; user drags the LEFT handle rightward past it. + let drag = ResizeDrag::begin(&doc, &r, id, Axis::X, AnchorEdge::End).unwrap(); + assert_eq!(drag.anchor, 200.0); + let n = ops::resize_drag(&mut doc, &r, id, &drag, 250.0).unwrap(); + assert_eq!(n, 3); + let r = resolve(&doc, &opts()); + assert_eq!(r.xywh(id), (200.0, 40.0, 50.0, 80.0)); + assert!(doc.get(id).header.flip_x); +} + +#[test] +fn f4_in_flow_drag_writes_extent_and_flip_only() { + let mut b = DocBuilder::new(); + let (fh, fp) = flex_row(460.0, 170.0); + let f = b.add(0, at(fh, 20.0, 20.0), fp); + let mut mid = 0; + for i in 0..3 { + let (ch, cp) = card(60.0, 100.0); + let id = b.add(f, ch, cp); + if i == 1 { + mid = id; + } + } + let mut doc = b.build(); + let r = resolve(&doc, &opts()); + let b0 = r.box_of(mid); + let drag = ResizeDrag::begin(&doc, &r, mid, Axis::X, AnchorEdge::Start).unwrap(); + // Cross past the anchor: under flex the position write is skipped — + // layout owns it — so the set shrinks to extent + flip. + let n = ops::resize_drag(&mut doc, &r, mid, &drag, b0.x - 30.0).unwrap(); + assert_eq!(n, 2); + assert!(doc.get(mid).header.flip_x); + assert_eq!(doc.get(mid).header.x, AxisBinding::default()); + // And the row re-lays out around the new 30px mirrored box. + let r = resolve(&doc, &opts()); + assert_eq!(r.xywh(mid).2, 30.0); +} + +// --- F-5: the typed wall ------------------------------------------------ + +#[test] +fn f5_typed_negative_size_is_rejected_untouched() { + let (mut doc, id) = free_rect(); + let before = doc.clone(); + assert_eq!( + ops::set_width(&mut doc, id, -50.0), + Err(OpError::NegativeExtent) + ); + assert_eq!( + ops::set_height(&mut doc, id, -0.5), + Err(OpError::NegativeExtent) + ); + assert_eq!( + doc, before, + "rejected op must leave the document byte-identical" + ); +} + +// --- F-6: ungroup bakes flips exactly ------------------------------------ + +#[test] +fn f6_ungroup_flipped_group_preserves_world() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(180.0); + gh.y = AxisBinding::start(120.0); + gh.rotation = 25.0; + gh.flip_x = true; + let g = b.add(0, gh, Payload::Group); + // A rotated boxed child and a flipped boxed child — both bake paths. + let (mut c1h, c1p) = card(70.0, 40.0); + c1h.rotation = 10.0; + let a = b.add(g, at(c1h, 10.0, 5.0), c1p); + let (mut c2h, c2p) = card(50.0, 50.0); + c2h.flip_y = true; + let s = b.add(g, at(c2h, 90.0, 60.0), c2p); + let mut doc = b.build(); + + let r0 = resolve(&doc, &opts()); + let wa0 = r0.world_of(a); + let ws0 = r0.world_of(s); + + ops::ungroup(&mut doc, &r0, g).unwrap(); + let r1 = resolve(&doc, &opts()); + assert!( + affine_approx(&r1.world_of(a), &wa0, 1e-3), + "rotated child drifted: {:?} vs {wa0:?}", + r1.world_of(a) + ); + assert!( + affine_approx(&r1.world_of(s), &ws0, 1e-3), + "flipped child drifted: {:?} vs {ws0:?}", + r1.world_of(s) + ); +} + +// --- IR round trip -------------------------------------------------------- + +#[test] +fn flip_survives_text_ir_round_trip() { + let (mut doc, id) = free_rect(); + doc.get_mut(id).header.flip_x = true; + doc.get_mut(id).header.rotation = 12.0; + let printed = n0_model::textir::print(&doc); + assert!(printed.contains("flip-x=\"true\"")); + let parsed = n0_model::textir::parse(&printed).unwrap(); + let reprinted = n0_model::textir::print(&parsed); + assert_eq!(printed, reprinted); +} diff --git a/crates/n0-model/tests/generations.rs b/crates/n0-model/tests/generations.rs new file mode 100644 index 00000000..5e015c95 --- /dev/null +++ b/crates/n0-model/tests/generations.rs @@ -0,0 +1,86 @@ +//! The per-slot generation column (engine setup, step 3; ENG-2.3). Live +//! nodes sit at generation 0; tombstoning a slot bumps it, so a future +//! reused slot cannot alias a prior node's cache key. That generations do +//! NOT affect document equality is proven by the existing MM-7 test +//! (`mm_laws.rs`) still passing — the semantic PartialEq never reads them. + +mod common; + +use n0_model::model::*; + +fn one_child() -> (Document, NodeId) { + let mut b = DocBuilder::new(); + let c = b.add( + 0, + Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + (b.build(), c) +} + +#[test] +fn live_nodes_are_generation_zero() { + let (doc, c) = one_child(); + assert_eq!(doc.gen_of(doc.root), 0); + assert_eq!(doc.gen_of(c), 0); + assert_eq!(doc.gen_of(9999), 0); // out of range reads 0 +} + +#[test] +fn tombstone_bumps_generation() { + let (mut doc, c) = one_child(); + let key = doc.key_of(c).unwrap(); + assert!(doc.contains_key(key)); + assert_eq!(doc.gen_of(c), 0); + let removed = doc.remove_subtree(c); + assert_eq!(removed, 1); + // The vacated slot's generation advanced; the root was untouched. + assert_eq!(doc.gen_of(c), 1); + assert_eq!(doc.gen_of(doc.root), 0); + assert!(!doc.contains_key(key)); + assert!(doc.node_for_key(key).is_none()); + assert!(doc.key_of(c).is_none()); +} + +#[test] +fn reused_slot_mints_a_new_generation_without_reviving_the_old_key() { + let (mut doc, child) = one_child(); + let old = doc.key_of(child).unwrap(); + doc.remove_subtree(child); + let replacement = Node::new( + child, + Header::new(SizeIntent::Fixed(2.0), SizeIntent::Fixed(3.0)), + Payload::Shape { + desc: ShapeDesc::Ellipse, + }, + ); + doc.add_child(doc.root, replacement); + + let current = doc.key_of(child).unwrap(); + assert_eq!(current.generation(), old.generation() + 1); + assert_ne!(current, old); + assert!(doc.contains_key(current)); + assert!(!doc.contains_key(old)); +} + +#[test] +fn keys_are_scoped_to_one_exact_document_arena() { + let (a, child_a) = one_child(); + let (b, child_b) = one_child(); + assert_eq!(child_a, child_b, "the arena slots deliberately coincide"); + let key_a = a.key_of(child_a).unwrap(); + assert!( + !b.contains_key(key_a), + "an identical fresh arena is not owner A" + ); + + let clone = a.clone(); + assert_eq!(clone, a, "arena identity is not semantic document state"); + assert!( + !clone.contains_key(key_a), + "a clone gets a new arena owner because it may diverge" + ); + assert_ne!(clone.key_of(child_a).unwrap().arena(), key_a.arena()); +} diff --git a/crates/n0-model/tests/geometry.rs b/crates/n0-model/tests/geometry.rs new file mode 100644 index 00000000..72ab60c3 --- /dev/null +++ b/crates/n0-model/tests/geometry.rs @@ -0,0 +1,175 @@ +//! G-* — geometry resolution across the binding/size vocabulary. + +mod common; +use common::*; + +use n0_model::model::*; +use n0_model::resolve::{resolve, Report}; + +fn one_child(parent_w: f32, parent_h: f32, h: Header, p: Payload) -> (Document, NodeId) { + let mut b = DocBuilder::new(); + let (mut fh, fp) = frame_free(SizeIntent::Fixed(parent_w), SizeIntent::Fixed(parent_h)); + fh.x = AxisBinding::start(0.0); + fh.y = AxisBinding::start(0.0); + let f = b.add(0, fh, fp); + let c = b.add(f, h, p); + (b.build(), c) +} + +/// G-1: the §2.1 resolution table, exact arithmetic. +#[test] +fn g1_binding_table_exact() { + // Pin{Start, 10} → x0 = 10 + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::start(10.0); + let (doc, c) = one_child(400.0, 300.0, h, p.clone()); + assert_close(run(&doc).box_of(c).x, 10.0, "start"); + + // Pin{End, 24} → x0 = 400 − 24 − 120 = 256 + let (mut h, _) = shape(120.0, 80.0); + h.x = AxisBinding::end(24.0); + let (doc, c) = one_child(400.0, 300.0, h, p.clone()); + assert_close(run(&doc).box_of(c).x, 256.0, "end"); + + // Pin{Center, 6} → x0 = (400 − 120)/2 + 6 = 146 + let (mut h, _) = shape(120.0, 80.0); + h.x = AxisBinding::center(6.0); + let (doc, c) = one_child(400.0, 300.0, h, p.clone()); + assert_close(run(&doc).box_of(c).x, 146.0, "center"); + + // Span{30, 50} → x0 = 30, w = 400 − 30 − 50 = 320 (SizeIntent ignored) + let (mut h, _) = shape(120.0, 80.0); + h.x = AxisBinding::Span { + start: 30.0, + end: 50.0, + }; + let (doc, c) = one_child(400.0, 300.0, h, p); + let b = run(&doc).box_of(c); + assert_close(b.x, 30.0, "span x"); + assert_close(b.w, 320.0, "span extent owns axis"); +} + +/// G-2: end-anchored intent survives parent resize — "right: 24" stays 24. +#[test] +fn g2_end_intent_survives_parent_resize() { + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::end(24.0); + let (mut doc, c) = one_child(400.0, 300.0, h, p); + let r1 = run(&doc); + assert_close(r1.box_of(c).x + 120.0 + 24.0, 400.0, "gap=24 @400"); + + // parent grows: same document field, re-resolved + let f = doc.parent_of(c).unwrap(); + doc.get_mut(f).header.width = SizeIntent::Fixed(640.0); + let r2 = run(&doc); + assert_close(r2.box_of(c).x + 120.0 + 24.0, 640.0, "gap=24 @640"); +} + +/// G-5: aspect-ratio resolves the under-specified axis only. +#[test] +fn g5_aspect_resolves_underspecified_axis() { + let mut h = Header::new(SizeIntent::Fixed(160.0), SizeIntent::Auto); + h.aspect_ratio = Some((16.0, 9.0)); + let (doc, c) = one_child( + 400.0, + 300.0, + h, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let r = run(&doc); + assert_close(r.box_of(c).h, 90.0, "h from 16:9"); + // and no error-by-rule fired for the auto axis + assert!( + !r.reports.iter().any(|rep| matches!( + rep, + Report::ErrorByRule { node, .. } if *node == c + )), + "aspect resolved the axis" + ); +} + +/// G-E1: zero-size parent — end/center anchors resolve, no NaN anywhere. +#[test] +fn ge1_zero_size_parent_no_nan() { + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::end(24.0); + h.y = AxisBinding::center(0.0); + let (doc, c) = one_child(0.0, 0.0, h, p); + let b = run(&doc).box_of(c); + assert!(b.x.is_finite() && b.y.is_finite()); + assert_close(b.x, -144.0, "end vs zero parent"); // 0 − 24 − 120 + assert_close(b.y, -40.0, "center vs zero parent"); // (0−80)/2 +} + +/// G-E2: span offsets exceeding the parent — declared clamp-to-zero, +/// reported, never a negative-width box downstream. +#[test] +fn ge2_negative_span_clamps_reported() { + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::Span { + start: 300.0, + end: 200.0, + }; + let (doc, c) = one_child(400.0, 300.0, h, p); + let r = run(&doc); + assert_close(r.box_of(c).w, 0.0, "clamped to zero"); + assert!( + r.reports + .iter() + .any(|rep| matches!(rep, Report::Clamped { node, .. } if *node == c)), + "clamp reported, not silent" + ); +} + +/// G-E3: Auto size on a kind with no natural size — declared error-by-rule, +/// never a silent 0-vs-unset ambiguity. +#[test] +fn ge3_auto_on_shape_is_error_by_rule() { + let h = Header::new(SizeIntent::Auto, SizeIntent::Fixed(80.0)); + let (doc, c) = one_child( + 400.0, + 300.0, + h, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let r = run(&doc); + assert!( + r.reports.iter().any(|rep| matches!( + rep, + Report::ErrorByRule { node, field: "width", .. } if *node == c + )), + "typed error surfaced" + ); +} + +/// G-4: min/max clamp applied after size resolution; min beats max. +#[test] +fn g4_min_max_clamp_declared() { + let (mut h, p) = shape(120.0, 80.0); + h.min_width = Some(200.0); + h.max_width = Some(150.0); + let (doc, c) = one_child(400.0, 300.0, h, p); + // min wins over max (declared G-4 rule) + assert_close(run(&doc).box_of(c).w, 200.0, "min beats max"); +} + +/// Root regularization (X-SELF-5 break): the scene root is an ordinary +/// frame; Span{0,0} bindings make it exactly the viewport. +#[test] +fn root_is_viewport_bound_frame() { + let b = DocBuilder::new(); + let doc = b.build(); + let r = resolve(&doc, &opts()); + assert_rect( + r.box_of(doc.root), + 0.0, + 0.0, + 1000.0, + 1000.0, + "root=viewport", + ); +} diff --git a/crates/n0-model/tests/layout.rs b/crates/n0-model/tests/layout.rs new file mode 100644 index 00000000..d72f9516 --- /dev/null +++ b/crates/n0-model/tests/layout.rs @@ -0,0 +1,384 @@ +//! L-* / T-* — flex behavior, hug, measured content, the §7 quartet (c). + +mod common; +use common::*; + +use n0_model::model::*; +use n0_model::resolve::{resolve, Report}; + +/// L-3: hug container with mixed fixed/grow children — no cycle; grow +/// distributes only definite free space (of which a hug container has none). +#[test] +fn l3_hug_with_grow_no_cycle() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(100.0), + Direction::Row, + 0.0, + 0.0, + ); + let f = b.add(0, h, p); + let (h1, p1) = shape(50.0, 50.0); + let a = b.add(f, h1, p1); + let (mut h2, p2) = shape(70.0, 50.0); + h2.grow = 1.0; + let g = b.add(f, h2, p2); + let doc = b.build(); + let r = run(&doc); + // hug = sum of basis sizes; grow had no free space to distribute + assert_close(r.box_of(f).w, 120.0, "hug from basis sizes"); + assert_close(r.box_of(a).w, 50.0, "fixed child"); + assert_close(r.box_of(g).w, 70.0, "grow child stays at basis under hug"); +} + +/// grow distributes definite free space (fixed container). +#[test] +fn grow_distributes_definite_free_space() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(300.0), + SizeIntent::Fixed(100.0), + Direction::Row, + 0.0, + 0.0, + ); + let f = b.add(0, h, p); + let (h1, p1) = shape(50.0, 50.0); + b.add(f, h1, p1); + let (mut h2, p2) = shape(70.0, 50.0); + h2.grow = 1.0; + let g = b.add(f, h2, p2); + let doc = b.build(); + let r = run(&doc); + assert_close(r.box_of(g).w, 250.0, "70 + (300−120) free"); +} + +/// L-4: absolute child inside a flex parent — excluded from flow, +/// resolves against the parent box. +#[test] +fn l4_absolute_child_in_flex() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(300.0), + SizeIntent::Fixed(100.0), + Direction::Row, + 10.0, + 10.0, + ); + let f = b.add(0, h, p); + let (h1, p1) = shape(50.0, 50.0); + let a = b.add(f, h1, p1); + let (mut h2, p2) = shape(40.0, 40.0); + h2.flow = Flow::Absolute; + h2.x = AxisBinding::end(8.0); + h2.y = AxisBinding::start(4.0); + let abs = b.add(f, h2, p2); + let (h3, p3) = shape(50.0, 50.0); + let c = b.add(f, h3, p3); + let doc = b.build(); + let r = run(&doc); + // flow children ignore the absolute one + assert_close(r.box_of(a).x, 10.0, "first flow child"); + assert_close(r.box_of(c).x, 70.0, "second flow child (no slot for abs)"); + // absolute resolves against the parent box + assert_close(r.box_of(abs).x, 300.0 - 8.0 - 40.0, "end-pinned in parent"); + assert_close(r.box_of(abs).y, 4.0, "start-pinned in parent"); +} + +/// L-5 / §7(c): measured child re-measures at the layout-imposed width — +/// text stretched by cross-align re-wraps. +#[test] +fn l5_text_rewraps_at_stretched_width() { + let mut b = DocBuilder::new(); + let (h, p) = { + let (mut h, p) = frame_flex( + SizeIntent::Fixed(100.0), + SizeIntent::Auto, + Direction::Column, + 8.0, + 0.0, + ); + if let Payload::Frame { layout, .. } = &p { + let mut l = *layout; + l.cross_align = CrossAlign::Stretch; + ( + h.clone(), + Payload::Frame { + layout: l, + clips_content: false, + }, + ) + } else { + unreachable!() + } + }; + let f = b.add(0, h, p); + // 10 chars/word world: "aaaa bbbb cccc" @10 → cw 6 + let th = Header::new(SizeIntent::Auto, SizeIntent::Auto); + let t = b.add( + f, + th, + Payload::Text { + content: "aaaa bbbb cccc".into(), + font_size: 10.0, + }, + ); + let doc = b.build(); + let r = run(&doc); + // stretched to container width 100 → 16 chars max → "aaaa bbbb cccc" is + // 14 chars → fits one line? 14*6=84 ≤ 100 → 1 line, h=12 + assert_close(r.box_of(t).w, 100.0, "stretched to container"); + assert_close(r.box_of(t).h, 12.0, "single line at 100"); + + // narrower container → re-wrap + let mut doc2 = doc.clone(); + doc2.get_mut(f).header.width = SizeIntent::Fixed(60.0); + let r2 = run(&doc2); + // 60/6 = 10 chars: "aaaa bbbb"(9) / "cccc" → 2 lines + assert_close(r2.box_of(t).h, 24.0, "re-wrapped at 60"); +} + +/// T-1: fixed-width text wraps; auto-width text measures single-line. +#[test] +fn t1_text_measurement_modes() { + let mut b = DocBuilder::new(); + let (fh, fp) = frame_free(SizeIntent::Fixed(500.0), SizeIntent::Fixed(500.0)); + let f = b.add(0, fh, fp); + + let auto_h = Header::new(SizeIntent::Auto, SizeIntent::Auto); + let t_auto = b.add( + f, + auto_h, + Payload::Text { + content: "hello world".into(), + font_size: 10.0, + }, + ); + let fixed_h = Header::new(SizeIntent::Fixed(36.0), SizeIntent::Auto); + let t_fixed = b.add( + f, + fixed_h, + Payload::Text { + content: "hello world".into(), + font_size: 10.0, + }, + ); + let doc = b.build(); + let r = run(&doc); + assert_close(r.box_of(t_auto).w, 66.0, "11 chars × 6"); + assert_close(r.box_of(t_auto).h, 12.0, "one line"); + assert_close(r.box_of(t_fixed).w, 36.0, "fixed width kept"); + assert_close(r.box_of(t_fixed).h, 24.0, "wrapped to two lines"); +} + +/// L-E1: empty container with padding + hug → size = padding box. +#[test] +fn le1_empty_hug_is_padding_box() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Auto, + Direction::Row, + 12.0, + 16.0, + ); + let f = b.add(0, h, p); + let doc = b.build(); + let r = run(&doc); + assert_rect(r.box_of(f), 0.0, 0.0, 32.0, 32.0, "padding box"); +} + +/// L-E3/L-E4: overflow — children exceed the fixed parent; no implicit +/// shrink (X-SELF-8 keep: flex_shrink = 0), overflow geometry exact. +#[test] +fn le3_no_implicit_shrink_on_overflow() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(100.0), + SizeIntent::Fixed(60.0), + Direction::Row, + 0.0, + 0.0, + ); + let f = b.add(0, h, p); + let mut ids = vec![]; + for _ in 0..3 { + let (h1, p1) = shape(50.0, 50.0); + ids.push(b.add(f, h1, p1)); + } + let doc = b.build(); + let r = run(&doc); + for (i, id) in ids.iter().enumerate() { + assert_close(r.box_of(*id).w, 50.0, "authored size kept"); + assert_close(r.box_of(*id).x, 50.0 * i as f32, "exact overflow geometry"); + } +} + +/// §8 matrix enforcement: x/y writes are ignored-by-rule under flow and the +/// resolver *reports* it (never silent). +#[test] +fn applicability_x_ignored_under_flow_reported() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(300.0), + SizeIntent::Fixed(100.0), + Direction::Row, + 0.0, + 0.0, + ); + let f = b.add(0, h, p); + let (mut h1, p1) = shape(50.0, 50.0); + h1.x = AxisBinding::start(77.0); // will be ignored: layout owns position + let a = b.add(f, h1, p1); + let doc = b.build(); + let r = run(&doc); + assert_close(r.box_of(a).x, 0.0, "layout owns x"); + assert!( + r.reports.iter().any(|rep| matches!( + rep, + Report::IgnoredByRule { node, field: "x/y", .. } if *node == a + )), + "ignored-by-rule reported" + ); +} + +/// Nested flex: row-in-column, fixed sizes, exact positions. +#[test] +fn nested_flex_exact() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(300.0), + SizeIntent::Auto, + Direction::Column, + 10.0, + 0.0, + ); + let outer = b.add(0, h, p); + let (h2, p2) = frame_flex(SizeIntent::Auto, SizeIntent::Auto, Direction::Row, 5.0, 0.0); + let inner = b.add(outer, h2, p2); + let (s1h, s1p) = shape(40.0, 30.0); + let s1 = b.add(inner, s1h, s1p); + let (s2h, s2p) = shape(40.0, 30.0); + let s2 = b.add(inner, s2h, s2p); + let (s3h, s3p) = shape(100.0, 20.0); + let s3 = b.add(outer, s3h, s3p); + let doc = b.build(); + let r = run(&doc); + assert_rect(r.box_of(inner), 0.0, 0.0, 85.0, 30.0, "inner hug 40+5+40"); + assert_close(r.box_of(s1).x, 0.0, "s1"); + assert_close(r.box_of(s2).x, 45.0, "s2"); + assert_close(r.box_of(s3).y, 40.0, "s3 after inner + gap"); + assert_close(r.box_of(outer).h, 60.0, "outer hug 30+10+20"); +} + +/// MM-8 analogue is N/A (no camera in the lab); viewport-bound root reacts, +/// free content doesn't — covered in mm_laws. This test pins the L-7 POL: +/// resolution is unquantized (no pixel snapping in the resolved tier). +#[test] +fn l7_resolution_unquantized() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(100.0), + SizeIntent::Fixed(30.0), + Direction::Row, + 0.0, + 0.0, + ); + let f = b.add(0, h, p); + for _ in 0..3 { + let (mut h1, p1) = shape(10.0, 10.0); + h1.grow = 1.0; + b.add(f, h1, p1); + } + let doc = b.build(); + let r = run(&doc); + let kids: Vec<_> = doc.get(f).children.clone(); + // 100 − 30 = 70 free / 3 = 23.333… — fractional, unsnapped + let w = r.box_of(kids[0]).w; + assert!((w - 33.333332).abs() < 1e-3, "fractional width kept: {w}"); + let _ = resolve(&doc, &opts_visual()); // both modes agree here +} + +#[test] +fn free_container_padding_insets_fixed_content_box() { + let mut b = DocBuilder::new(); + let (h, p) = frame_free(SizeIntent::Fixed(100.0), SizeIntent::Fixed(80.0)); + let p = match p { + Payload::Frame { + mut layout, + clips_content, + } => { + layout.padding = EdgeInsets { + top: 7.0, + right: 20.0, + bottom: 13.0, + left: 10.0, + }; + Payload::Frame { + layout, + clips_content, + } + } + _ => unreachable!(), + }; + let frame = b.add(0, h, p); + + let (mut child_h, child_p) = shape(20.0, 10.0); + child_h.x = AxisBinding::start(5.0); + child_h.y = AxisBinding::start(6.0); + let child = b.add(frame, child_h, child_p); + + let mut span_h = Header::new(SizeIntent::Auto, SizeIntent::Fixed(10.0)); + span_h.x = AxisBinding::Span { + start: 0.0, + end: 0.0, + }; + span_h.y = AxisBinding::start(30.0); + let span = b.add( + frame, + span_h, + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + + let r = run(&b.build()); + assert_rect(r.box_of(child), 15.0, 13.0, 20.0, 10.0, "padded start"); + assert_rect( + r.box_of(span), + 10.0, + 37.0, + 70.0, + 10.0, + "span fills padded content width", + ); +} + +#[test] +fn free_container_padding_insets_auto_content_box() { + let mut b = DocBuilder::new(); + let (h, p) = frame_free(SizeIntent::Auto, SizeIntent::Auto); + let p = match p { + Payload::Frame { + mut layout, + clips_content, + } => { + layout.padding = EdgeInsets::all(10.0); + Payload::Frame { + layout, + clips_content, + } + } + _ => unreachable!(), + }; + let frame = b.add(0, h, p); + let (mut child_h, child_p) = shape(20.0, 30.0); + child_h.x = AxisBinding::start(5.0); + child_h.y = AxisBinding::start(7.0); + let child = b.add(frame, child_h, child_p); + + let r = run(&b.build()); + assert_rect(r.box_of(frame), 0.0, 0.0, 45.0, 57.0, "auto padded frame"); + assert_rect(r.box_of(child), 15.0, 17.0, 20.0, 30.0, "auto padded child"); +} diff --git a/crates/n0-model/tests/lens_ops_properties.rs b/crates/n0-model/tests/lens_ops_properties.rs new file mode 100644 index 00000000..f4523c59 --- /dev/null +++ b/crates/n0-model/tests/lens_ops_properties.rs @@ -0,0 +1,189 @@ +//! Whole-list lens-operation property seam. + +use n0_model::model::*; +use n0_model::pick::pick; +use n0_model::properties::*; +use n0_model::resolve::{resolve, resolve_view, ResolveOptions}; + +fn target(document: &Document, node: NodeId) -> PropertyTarget { + PropertyTarget::new(document.key_of(node).unwrap(), PropertyKey::LensOps) +} + +fn lens_scene() -> (Document, NodeId, NodeId, NodeId) { + let mut builder = DocBuilder::new(); + let frame = builder.add( + 0, + Header::new(SizeIntent::Fixed(200.0), SizeIntent::Fixed(40.0)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + direction: Direction::Row, + gap_main: 10.0, + ..LayoutBehavior::default() + }, + clips_content: false, + }, + ); + let lens = builder.add( + frame, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { ops: vec![] }, + ); + let child = builder.add( + lens, + Header::new(SizeIntent::Fixed(20.0), SizeIntent::Fixed(20.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let sibling = builder.add( + frame, + Header::new(SizeIntent::Fixed(20.0), SizeIntent::Fixed(20.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + (builder.build(), lens, child, sibling) +} + +#[test] +fn lens_ops_is_a_lens_only_visual_transform_property() { + let mut builder = DocBuilder::new(); + let lens = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { + ops: vec![LensOp::Translate { x: 3.0, y: 4.0 }], + }, + ); + let shape = builder.add( + lens, + Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let document = builder.build(); + let spec = PropertyKey::LensOps.spec(); + + assert_eq!(spec.value_kind, PropertyValueKind::LensOps); + assert_eq!(spec.applicability, PropertyApplicability::Lens); + assert!(spec.applies_to(document.get(lens))); + assert!(!spec.applies_to(document.get(shape))); + assert_eq!( + spec.impact.bits(), + PropertyImpact::TRANSFORM.bits() + | PropertyImpact::BOUNDS.bits() + | PropertyImpact::PAINT.bits() + ); + assert_eq!( + spec.base_value(document.get(lens)), + Some(PropertyValue::LensOps(vec![LensOp::Translate { + x: 3.0, + y: 4.0 + }])) + ); + assert_eq!( + ValueView::base(&document).lens_ops(lens), + [LensOp::Translate { x: 3.0, y: 4.0 }] + ); + + let error = PropertyValues::new( + &document, + [(target(&document, shape), PropertyValue::LensOps(vec![]))], + ) + .unwrap_err(); + assert!(matches!(error, PropertyError::Inapplicable { .. })); +} + +#[test] +fn lens_ops_validation_rejects_only_non_finite_parameters() { + let mut builder = DocBuilder::new(); + let lens = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { ops: vec![] }, + ); + builder.add( + lens, + Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let document = builder.build(); + let target = target(&document, lens); + + let valid = vec![ + LensOp::Translate { x: -2.0, y: 3.0 }, + LensOp::Rotate { deg: -450.0 }, + LensOp::Scale { x: 0.0, y: -2.0 }, + LensOp::Skew { + x_deg: 89.0, + y_deg: -89.0, + }, + LensOp::Matrix { + m: [-1.0, 0.0, 0.0, 0.0, 4.0, 5.0], + }, + ]; + PropertyValues::new(&document, [(target, PropertyValue::LensOps(valid))]).unwrap(); + + for invalid in [ + LensOp::Translate { + x: f32::NAN, + y: 0.0, + }, + LensOp::Rotate { deg: f32::INFINITY }, + LensOp::Scale { + x: 1.0, + y: f32::NEG_INFINITY, + }, + LensOp::Skew { + x_deg: f32::NAN, + y_deg: 0.0, + }, + LensOp::Matrix { + m: [1.0, 0.0, 0.0, 1.0, f32::INFINITY, 0.0], + }, + ] { + let error = + PropertyValues::new(&document, [(target, PropertyValue::LensOps(vec![invalid]))]) + .unwrap_err(); + assert!( + matches!(&error, PropertyError::InvalidValue { reason, .. } if reason.contains("lens operation 0") && reason.contains("finite")), + "{error}" + ); + } +} + +#[test] +fn effective_lens_ops_change_reads_but_not_layout() { + let (document, lens, child, sibling) = lens_scene(); + let options = ResolveOptions { + viewport: (200.0, 40.0), + ..ResolveOptions::default() + }; + let base = resolve(&document, &options); + let values = PropertyValues::new( + &document, + [( + target(&document, lens), + PropertyValue::LensOps(vec![LensOp::Translate { x: 100.0, y: 0.0 }]), + )], + ) + .unwrap(); + let view = ValueView::new(&document, &values).unwrap(); + let effective = resolve_view(&view, &options); + + assert_eq!( + view.lens_ops(lens), + [LensOp::Translate { x: 100.0, y: 0.0 }] + ); + assert_eq!(effective.box_of(lens), base.box_of(lens)); + assert_eq!(effective.box_of(sibling), base.box_of(sibling)); + assert_eq!(effective.world_of(lens), base.world_of(lens)); + assert_eq!(effective.world_of(child).e, base.world_of(child).e + 100.0); + assert_eq!(pick(&base, 5.0, 5.0), Some(lens)); + assert_ne!(pick(&effective, 5.0, 5.0), Some(lens)); + assert_eq!(pick(&effective, 105.0, 5.0), Some(lens)); +} diff --git a/crates/n0-model/tests/mm_laws.rs b/crates/n0-model/tests/mm_laws.rs new file mode 100644 index 00000000..0537974a --- /dev/null +++ b/crates/n0-model/tests/mm_laws.rs @@ -0,0 +1,217 @@ +//! Metamorphic laws (conformance.md §1) — properties over pairs of documents. + +mod common; +use common::*; + +use n0_model::model::*; +use n0_model::ops; +use n0_model::resolve::resolve; + +fn sample_doc() -> (Document, NodeId, NodeId, NodeId) { + let mut b = DocBuilder::new(); + let (fh, fp) = frame_free(SizeIntent::Fixed(400.0), SizeIntent::Fixed(300.0)); + let mut fh = fh; + fh.x = AxisBinding::start(50.0); + fh.y = AxisBinding::start(60.0); + let f = b.add(0, fh, fp); + let (mut sh, sp) = shape(120.0, 80.0); + sh.x = AxisBinding::start(10.0); + sh.y = AxisBinding::start(20.0); + sh.rotation = 15.0; + let s = b.add(f, sh, sp); + let (mut th, tp) = shape(40.0, 40.0); + th.x = AxisBinding::end(24.0); + th.y = AxisBinding::center(0.0); + let t = b.add(f, th, tp); + (b.build(), f, s, t) +} + +/// MM-1: translating a parent translates every descendant world transform +/// by exactly (dx,dy) and nothing else. +#[test] +fn mm1_parent_translation_propagates_exactly() { + let (doc, f, s, t) = sample_doc(); + let r1 = run(&doc); + + let mut doc2 = doc.clone(); + let r0 = run(&doc2); + ops::move_by(&mut doc2, &r0, f, 7.0, -13.0).unwrap(); + let r2 = run(&doc2); + + for id in [f, s, t] { + let w1 = r1.world_of(id); + let w2 = r2.world_of(id); + assert_close(w2.e - w1.e, 7.0, "dx"); + assert_close(w2.f - w1.f, -13.0, "dy"); + assert_close(w1.a, w2.a, "a unchanged"); + assert_close(w1.b, w2.b, "b unchanged"); + assert_close(w1.c, w2.c, "c unchanged"); + assert_close(w1.d, w2.d, "d unchanged"); + } +} + +/// MM-2: rotate(θ) then rotate(−θ) restores identical resolved geometry. +#[test] +fn mm2_rotation_roundtrip_restores_geometry() { + let (doc, _, s, _) = sample_doc(); + let r1 = run(&doc); + + let mut doc2 = doc.clone(); + ops::set_rotation(&mut doc2, s, 15.0 + 33.0).unwrap(); + ops::set_rotation(&mut doc2, s, 15.0).unwrap(); + let r2 = run(&doc2); + + assert_eq!(doc, doc2, "document restored byte-for-byte"); + let (w1, w2) = (r1.world_of(s), r2.world_of(s)); + assert_eq!( + (w1.a, w1.b, w1.c, w1.d, w1.e, w1.f), + (w2.a, w2.b, w2.c, w2.d, w2.e, w2.f), + "resolved world identical" + ); +} + +/// MM-3: resolution is a pure function — identical output run-to-run. +#[test] +fn mm3_determinism() { + let (doc, _, s, t) = sample_doc(); + let r1 = run(&doc); + let r2 = run(&doc); + for id in [s, t] { + assert_eq!(r1.world_of(id), r2.world_of(id)); + assert_eq!(r1.box_of(id), r2.box_of(id)); + assert_eq!(r1.aabb_of(id), r2.aabb_of(id)); + } +} + +/// MM-4: a free node's geometry is viewport-independent (only the +/// viewport-bound root's subtree sizing paths may react). +#[test] +fn mm4_viewport_independence_of_fixed_free_nodes() { + let (doc, f, s, _) = sample_doc(); + let r1 = resolve(&doc, &opts()); + let mut o2 = opts(); + o2.viewport = (640.0, 480.0); + let r2 = resolve(&doc, &o2); + // f is Start-pinned with Fixed size: unaffected by viewport. + assert_eq!(r1.box_of(f), r2.box_of(f)); + assert_eq!(r1.world_of(s), r2.world_of(s)); +} + +/// MM-5: writes to independent fields commute. +#[test] +fn mm5_independent_writes_commute() { + let (doc, _, s, t) = sample_doc(); + + let mut d1 = doc.clone(); + let r = run(&d1); + ops::set_rotation(&mut d1, s, 45.0).unwrap(); + ops::set_width(&mut d1, t, 55.0).unwrap(); + let _ = r; + + let mut d2 = doc.clone(); + ops::set_width(&mut d2, t, 55.0).unwrap(); + ops::set_rotation(&mut d2, s, 45.0).unwrap(); + + assert_eq!(d1, d2, "A;B ≡ B;A on the document"); +} + +/// MM-6 (declared POL): hidden children do not affect siblings' geometry. +#[test] +fn mm6_hidden_child_does_not_move_siblings() { + let mut b = DocBuilder::new(); + let f = b.add( + 0, + frame_flex( + SizeIntent::Fixed(500.0), + SizeIntent::Fixed(100.0), + Direction::Row, + 10.0, + 0.0, + ) + .0, + frame_flex( + SizeIntent::Fixed(500.0), + SizeIntent::Fixed(100.0), + Direction::Row, + 10.0, + 0.0, + ) + .1, + ); + let (h1, p1) = shape(50.0, 50.0); + let a = b.add(f, h1, p1); + let (h2, p2) = shape(50.0, 50.0); + let hidden = b.add(f, h2, p2); + let (h3, p3) = shape(50.0, 50.0); + let c = b.add(f, h3, p3); + let doc_visible = b.build(); + + let mut doc_hidden = doc_visible.clone(); + doc_hidden.get_mut(hidden).header.active = false; + + let rv = run(&doc_visible); + let rh = run(&doc_hidden); + // with the middle child hidden, c moves to where the hidden one was + assert_close(rv.box_of(a).x, rh.box_of(a).x, "a stable"); + assert_close(rv.box_of(hidden).x, rh.box_of(c).x, "c takes the slot"); + assert!(rh.world_opt(hidden).is_none(), "hidden not resolved"); +} + +/// MM-7: adding then deleting a node restores prior resolved geometry. +#[test] +fn mm7_add_delete_restores() { + let (doc, f, s, t) = sample_doc(); + let r1 = run(&doc); + + let mut doc2 = doc.clone(); + let new_id = 999; // sparse id: the arena grows, and semantic + // equality ignores the tombstoned tail (MM-7). + doc2.add_child( + f, + Node::new(new_id, shape(10.0, 10.0).0, shape(10.0, 10.0).1), + ); + let _mid = run(&doc2); + doc2.remove_subtree(new_id); + assert_eq!(doc, doc2); + let r2 = run(&doc2); + for id in [s, t] { + assert_eq!(r1.world_of(id), r2.world_of(id)); + } +} + +/// MM-9 (via the sanctioned ungroup bake): composed local placement +/// preserves world transforms within tolerance (also D-4). +#[test] +fn mm9_ungroup_preserves_world_transforms() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(100.0); + gh.y = AxisBinding::start(50.0); + gh.rotation = 30.0; + let g = b.add(0, gh, Payload::Group); + let (mut s1h, s1p) = shape(40.0, 40.0); + s1h.rotation = 10.0; + let s1 = b.add(g, s1h, s1p); + let (mut s2h, s2p) = shape(40.0, 40.0); + s2h.x = AxisBinding::start(56.0); + let s2 = b.add(g, s2h, s2p); + let mut doc = b.build(); + + let before = run(&doc); + let w1 = before.world_of(s1); + let w2 = before.world_of(s2); + + n0_model::ops::ungroup(&mut doc, &before, g).unwrap(); + assert!(doc.get_opt(g).is_none(), "group dissolved"); + + let after = run(&doc); + for (id, w) in [(s1, w1), (s2, w2)] { + let v = after.world_of(id); + assert_close(v.a, w.a, "a"); + assert_close(v.b, w.b, "b"); + assert_close(v.c, w.c, "c"); + assert_close(v.d, w.d, "d"); + assert_close(v.e, w.e, "e"); + assert_close(v.f, w.f, "f"); + } +} diff --git a/crates/n0-model/tests/n0_xml_attributed_text.rs b/crates/n0-model/tests/n0_xml_attributed_text.rs new file mode 100644 index 00000000..df698a42 --- /dev/null +++ b/crates/n0-model/tests/n0_xml_attributed_text.rs @@ -0,0 +1,326 @@ +//! Focused contract for the flat `` + direct-child `` surface. + +use n0_model::model::*; +use n0_model::n0_xml; + +fn parse_text(source: &str) -> (Document, NodeId) { + let source = format!("{source}"); + let doc = n0_xml::parse(&source).expect("attributed text source parses"); + let container = doc.get(doc.root).children[0]; + let text = doc.get(container).children[0]; + (doc, text) +} + +fn attributed(node: &Node) -> (&AttributedString, TextStyleRec) { + match &node.payload { + Payload::AttributedText { + attributed_string, + default_style, + } => (attributed_string, *default_style), + payload => panic!("expected attributed text, got {payload:?}"), + } +} + +#[test] +fn mixed_content_lowers_to_complete_utf8_byte_runs() { + let (doc, text_id) = parse_text( + r##" A🙂 +Z"##, + ); + let (value, default_style) = attributed(doc.get(text_id)); + + assert_eq!(value.text, " A🙂中\nZ"); + assert_eq!( + default_style, + TextStyleRec { + font_size: 18.0, + font_weight: 500, + font_style_italic: true, + } + ); + assert_eq!(value.runs.len(), 3); + assert_eq!((value.runs[0].start, value.runs[0].end), (0, 6)); + assert_eq!((value.runs[1].start, value.runs[1].end), (6, 9)); + assert_eq!((value.runs[2].start, value.runs[2].end), (9, 11)); + assert_eq!(value.run_text(&value.runs[0]), " A🙂"); + assert_eq!(value.run_text(&value.runs[1]), "中"); + assert_eq!(value.run_text(&value.runs[2]), "\nZ"); + assert_eq!(value.runs[0].style, default_style); + assert_eq!(value.runs[2].style, default_style); + assert_eq!( + value.runs[1].style, + TextStyleRec { + font_size: 24.0, + font_weight: 700, + font_style_italic: false, + } + ); + let fills = value.runs[1].fills.as_ref().expect("explicit run fill"); + let [Paint::Solid(solid)] = fills.as_slice() else { + panic!("solid shorthand must lower to one solid paint") + }; + assert_eq!(solid.color.to_hex(), "#FF0000"); + + let printed = n0_xml::print(&doc).expect("attributed text prints"); + assert!(printed.contains( + r##" A🙂 +Z"## + )); + let reparsed = n0_xml::parse(&printed).unwrap(); + assert_eq!(printed, n0_xml::print(&reparsed).unwrap()); +} + +#[test] +fn adjacent_equal_runs_coalesce_and_semantically_uniform_markup_disappears() { + let (doc, text_id) = parse_text( + r#"xaby"#, + ); + let (value, _) = attributed(doc.get(text_id)); + assert_eq!(value.text, "xaby"); + assert_eq!(value.runs.len(), 3); + assert_eq!(value.run_text(&value.runs[1]), "ab"); + + let printed = n0_xml::print(&doc).unwrap(); + assert_eq!(printed.matches("ab"#), + "{printed}" + ); + + let (uniform, uniform_id) = parse_text(r#"abc"#); + assert!(matches!( + &uniform.get(uniform_id).payload, + Payload::Text { content, font_size } + if content == "abc" && *font_size == TextStyleRec::DEFAULT_FONT_SIZE + )); + let uniform_printed = n0_xml::print(&uniform).unwrap(); + assert!(!uniform_printed.contains("ABCD"##, + ); + let (value, _) = attributed(doc.get(text_id)); + assert_eq!(value.text, "ABCD"); + assert_eq!(value.runs.len(), 4); + + let painted = value.runs[1].fills.as_ref().expect("paint override"); + assert_eq!(painted.len(), 3); + assert!(matches!(painted[0], Paint::Solid(_))); + assert!(matches!(painted[1], Paint::LinearGradient(_))); + let Paint::Image(image) = &painted[2] else { + panic!("third run paint must be the image resource") + }; + assert_eq!(image.image, ResourceRef::Rid("./texture.png".into())); + assert_eq!(image.fit, ImagePaintFit::Fit(BoxFit::Cover)); + assert_eq!(image.opacity, 0.25); + assert!(value.runs[2] + .fills + .as_ref() + .expect("explicit empty override") + .is_empty()); + assert!(value.runs[0].fills.is_none()); + assert!(value.runs[3].fills.is_none()); + + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains(""), "{printed}"); + assert!( + printed.contains(r#""#), + "{printed}" + ); + assert!(printed.contains("B"), "{printed}"); + assert!(printed.contains("C"), "{printed}"); + let reparsed = n0_xml::parse(&printed).unwrap(); + assert_eq!(printed, n0_xml::print(&reparsed).unwrap()); +} + +#[test] +fn canonical_writer_normalizes_programmatic_run_boundaries() { + let (mut doc, text_id) = parse_text("ab"); + let default_style = TextStyleRec::default(); + let bold = TextStyleRec { + font_weight: 700, + ..default_style + }; + let attributed_string = AttributedString::from_runs( + "ab", + vec![ + StyledTextRun { + start: 0, + end: 1, + style: bold, + fills: None, + }, + StyledTextRun { + start: 1, + end: 2, + style: bold, + fills: None, + }, + ], + ) + .unwrap(); + doc.get_mut(text_id).payload = Payload::AttributedText { + attributed_string, + default_style, + }; + + let printed = n0_xml::print(&doc).expect("writer compares normalized text semantics"); + assert_eq!(printed.matches("ab"#), + "{printed}" + ); + assert_eq!( + printed, + n0_xml::print(&n0_xml::parse(&printed).unwrap()).unwrap() + ); +} + +#[test] +fn attributed_text_rejects_lossy_or_ambiguous_inline_syntax() { + let cases = [ + ( + r#"x"#, + "nested ", + ), + (r#""#, "at least one character"), + (r#""#, "at least one character"), + ( + r#"x"#, + "no lossless attributed-text destination", + ), + ( + r#"
"#, + "use explicit styling and a newline character", + ), + (r#"x"#, "use "), + ( + r#"x"#, + "SVG positioning attribute `x`", + ), + ( + r#"x"#, + "SVG positioning attribute `dx`", + ), + ( + r#"x"#, + "integer from 1 through 1000", + ), + ( + r#"x"#, + "integer from 1 through 1000", + ), + ( + r#"x"#, + "integer from 1 through 1000", + ), + ( + r#"x"#, + "must be `normal` or `italic`", + ), + ( + r#"x"#, + "font-size on must be greater than zero", + ), + ( + r#"x"#, + "unknown attribute `style` on ", + ), + ( + r#"x"#, + "unknown attribute `width` on ", + ), + ( + r#" x"#, + "must be the first child/event", + ), + ( + r#"x"#, + "must be the first child/event", + ), + ( + r##"x"##, + "both the `fill` attribute and ", + ), + ( + r#"x"#, + "run stroke geometry", + ), + ( + r#"x"#, + "not allowed inside ", + ), + (r#"x"#, "use `font-size`"), + ]; + + for (text, expected) in cases { + let source = format!("{text}"); + let error = n0_xml::parse(&source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` for {text}, got {error}" + ); + } + + let outside = + n0_xml::parse(r#"x"#) + .unwrap_err(); + assert!(outside.to_string().contains("direct child of ")); +} + +#[test] +fn attributed_string_validation_is_utf8_and_override_aware() { + let style = TextStyleRec::default(); + let bad_boundary = AttributedString::from_runs( + "🙂x", + vec![ + StyledTextRun { + start: 0, + end: 1, + style, + fills: None, + }, + StyledTextRun { + start: 1, + end: 5, + style, + fills: None, + }, + ], + ) + .unwrap_err(); + assert!(bad_boundary.contains("UTF-8 character boundaries")); + + let mut value = AttributedString::from_runs( + "abc", + vec![ + StyledTextRun { + start: 0, + end: 1, + style, + fills: None, + }, + StyledTextRun { + start: 1, + end: 2, + style, + fills: None, + }, + StyledTextRun { + start: 2, + end: 3, + style, + fills: Some(Paints::default()), + }, + ], + ) + .unwrap(); + value.merge_adjacent_runs(); + assert_eq!(value.runs.len(), 2); + assert_eq!((value.runs[0].start, value.runs[0].end), (0, 2)); + assert!(value.runs[0].fills.is_none()); + assert!(value.runs[1].fills.as_ref().unwrap().is_empty()); +} diff --git a/crates/n0-model/tests/n0_xml_corners.rs b/crates/n0-model/tests/n0_xml_corners.rs new file mode 100644 index 00000000..0a520996 --- /dev/null +++ b/crates/n0-model/tests/n0_xml_corners.rs @@ -0,0 +1,219 @@ +//! Draft 0 rounded-box syntax is a lossless projection of Grida's existing +//! per-corner elliptical radii plus normalized corner smoothing. + +use n0_model::model::*; +use n0_model::n0_xml::{self, PrintError}; +use n0_model::resolve::{resolve, ResolveOptions}; +use n0_model::{svgout, textir}; + +fn canonical(source: &str) -> (Document, String) { + let doc = n0_xml::parse(source).expect("Draft 0 source parses"); + let printed = n0_xml::print(&doc).expect("parsed Draft 0 source prints"); + let reparsed = n0_xml::parse(&printed).expect("canonical source reparses"); + assert_eq!(doc, reparsed, "semantic round-trip\n---\n{printed}"); + assert_eq!( + printed, + n0_xml::print(&reparsed).expect("canonical source reprints"), + "writer fixpoint" + ); + (doc, printed) +} + +fn authored_root(doc: &Document) -> NodeId { + doc.get(doc.root).children[0] +} + +fn one_rect(attributes: &str) -> String { + format!( + "" + ) +} + +#[test] +fn per_corner_elliptical_radii_materialize_in_tl_tr_br_bl_order() { + let (doc, printed) = canonical(&one_rect(r#"corner-radius="1 2 3 4 / 5 6 7 8""#)); + let rect = doc.get(authored_root(&doc)).children[0]; + assert_eq!( + doc.get(rect).corner_radius, + RectangularCornerRadius { + tl: Radius { rx: 1.0, ry: 5.0 }, + tr: Radius { rx: 2.0, ry: 6.0 }, + br: Radius { rx: 3.0, ry: 7.0 }, + bl: Radius { rx: 4.0, ry: 8.0 }, + } + ); + assert!( + printed.contains(r#"corner-radius="1 2 3 4 / 5 6 7 8""#), + "{printed}" + ); +} + +#[test] +fn canonical_writer_compresses_only_the_defined_one_or_four_value_grammar() { + let (_, uniform) = canonical(&one_rect(r#"corner-radius="9 9 9 9""#)); + assert!(uniform.contains(r#"corner-radius="9""#), "{uniform}"); + + let (_, elliptical) = canonical(&one_rect(r#"corner-radius="5 5 5 5 / 6 6 6 6""#)); + assert!( + elliptical.contains(r#"corner-radius="5 / 6""#), + "{elliptical}" + ); + + let (_, circular) = canonical(&one_rect(r#"corner-radius="1 2 3 4 / 1 2 3 4""#)); + assert!( + circular.contains(r#"corner-radius="1 2 3 4""#), + "{circular}" + ); + assert!(!circular.contains(" / "), "{circular}"); +} + +#[test] +fn smoothing_is_normalized_and_may_be_dormant_with_zero_radii() { + let (doc, printed) = canonical(&one_rect(r#"corner-smoothing="0.6""#)); + let rect = doc.get(authored_root(&doc)).children[0]; + assert_eq!( + doc.get(rect).corner_radius, + RectangularCornerRadius::default() + ); + assert_eq!(doc.get(rect).corner_smoothing, CornerSmoothing(0.6)); + assert!(printed.contains(r#"corner-smoothing="0.6""#), "{printed}"); + + let (_, rounded) = canonical(&one_rect( + r#"corner-radius="12 18 24 30" corner-smoothing="1""#, + )); + assert!(rounded.contains(r#"corner-smoothing="1""#), "{rounded}"); +} + +#[test] +fn explicit_zero_corner_defaults_are_omitted() { + let (_, printed) = canonical(&one_rect( + r#"corner-radius="0 0 0 0 / 0" corner-smoothing="0""#, + )); + assert!(!printed.contains("corner-radius="), "{printed}"); + assert!(!printed.contains("corner-smoothing="), "{printed}"); +} + +#[test] +fn malformed_corner_values_have_focused_parse_errors() { + for (attributes, expected) in [ + (r#"corner-radius="""#, "takes 1 or 4 numbers"), + (r#"corner-radius="1 2""#, "takes 1 or 4 numbers"), + (r#"corner-radius="1 2 3""#, "takes 1 or 4 numbers"), + (r#"corner-radius="1 2 3 4 5""#, "takes 1 or 4 numbers"), + (r#"corner-radius="/ 2""#, "both sides"), + (r#"corner-radius="2 /""#, "both sides"), + (r#"corner-radius="1 / 2 / 3""#, "at most one"), + (r#"corner-radius="-1""#, "non-negative"), + (r#"corner-radius="NaN""#, "non-finite"), + (r#"corner-smoothing="-0.1""#, "between 0 and 1"), + (r#"corner-smoothing="1.1""#, "between 0 and 1"), + (r#"corner-smoothing="NaN""#, "non-finite"), + ] { + let error = n0_xml::parse(&one_rect(attributes)).unwrap_err(); + assert!( + error.to_string().contains(expected), + "{attributes}: expected `{expected}`, got `{error}`" + ); + } +} + +#[test] +fn corner_attributes_are_box_primitive_only() { + for node in [ + r#""#, + r#""#, + r#"x"#, + r#""#, + ] { + let source = format!("{node}"); + let error = n0_xml::parse(&source).unwrap_err(); + assert!( + error + .to_string() + .contains("only valid on and "), + "{node}: {error}" + ); + } + + canonical( + r#""#, + ); +} + +#[test] +fn smooth_elliptical_corners_are_rejected_instead_of_narrowed() { + let error = n0_xml::parse(&one_rect( + r#"corner-radius="12 / 8" corner-smoothing="0.5""#, + )) + .unwrap_err(); + assert!(error.to_string().contains("requires circular"), "{error}"); +} + +#[test] +fn writer_validates_programmatic_corner_states() { + let mut doc = n0_xml::parse(&one_rect("")).unwrap(); + let rect = doc.get(authored_root(&doc)).children[0]; + + doc.get_mut(rect).corner_radius = RectangularCornerRadius::circular(-1.0); + let error = n0_xml::print(&doc).unwrap_err(); + assert!( + error.to_string().contains("negative corner radii"), + "{error}" + ); + + doc.get_mut(rect).corner_radius = RectangularCornerRadius::default(); + doc.get_mut(rect).corner_smoothing = CornerSmoothing(f32::NAN); + let error = n0_xml::print(&doc).unwrap_err(); + assert!(error.to_string().contains("must be finite"), "{error}"); + + doc.get_mut(rect).corner_radius = RectangularCornerRadius::all(Radius { rx: 8.0, ry: 4.0 }); + doc.get_mut(rect).corner_smoothing = CornerSmoothing(0.4); + let error = n0_xml::print(&doc).unwrap_err(); + assert!(error.to_string().contains("cannot render"), "{error}"); + + let mut ellipse = n0_xml::parse( + r#""#, + ) + .unwrap(); + let ellipse_id = ellipse.get(authored_root(&ellipse)).children[0]; + ellipse.get_mut(ellipse_id).corner_radius = RectangularCornerRadius::circular(2.0); + let error = n0_xml::print(&ellipse).unwrap_err(); + assert!(error.to_string().contains("cannot carry"), "{error}"); +} + +#[test] +fn implicit_document_root_corner_state_is_part_of_canonicality() { + let mut doc = n0_xml::parse(r#""#).unwrap(); + doc.get_mut(doc.root).corner_radius = RectangularCornerRadius::circular(1.0); + assert_eq!( + n0_xml::print(&doc), + Err(PrintError::NonCanonicalDocumentRoot) + ); +} + +#[test] +fn historical_textir_and_svg_snapshot_export_refuse_corner_loss() { + let mut historical = + textir::parse(r#""#) + .unwrap(); + historical.get_mut(1).corner_radius = RectangularCornerRadius::circular(3.0); + let text_error = textir::try_print(&historical).unwrap_err(); + assert!( + text_error.to_string().contains("cannot represent"), + "{text_error}" + ); + + let doc = n0_xml::parse(&one_rect(r#"corner-radius="3""#)).unwrap(); + let resolved = resolve(&doc, &ResolveOptions::default()); + let svg_error = svgout::render( + &doc, + &resolved, + &svgout::SvgOptions { + show_aabb: false, + width: 100.0, + height: 100.0, + }, + ) + .unwrap_err(); + assert!(svg_error.to_string().contains("losslessly"), "{svg_error}"); +} diff --git a/crates/n0-model/tests/n0_xml_paints.rs b/crates/n0-model/tests/n0_xml_paints.rs new file mode 100644 index 00000000..1f8e9b41 --- /dev/null +++ b/crates/n0-model/tests/n0_xml_paints.rs @@ -0,0 +1,586 @@ +//! Producer contract for ordered typed paints in Draft 0 `.n0.xml`. + +use n0_model::model::*; +use n0_model::n0_xml::{self, PrintError}; + +const THREE_LAYER: &str = r##" + + + + + + + + + + + + Example + + + +"##; + +fn named(doc: &Document, name: &str) -> NodeId { + (0..doc.capacity() as NodeId) + .find(|id| { + doc.get_opt(*id) + .is_some_and(|node| node.header.name.as_deref() == Some(name)) + }) + .expect("named node") +} + +#[test] +fn typed_fill_lowers_to_the_ordered_production_variants() { + let doc = n0_xml::parse(THREE_LAYER).expect("typed fills parse"); + let card = doc.get(named(&doc, "card")); + assert_eq!(card.fills.len(), 3); + + let Paint::Solid(solid) = &card.fills[0] else { + panic!("first paint must be solid"); + }; + assert_eq!(solid.color.to_hex(), "#101828"); + + let Paint::LinearGradient(linear) = &card.fills[1] else { + panic!("second paint must be linear gradient"); + }; + assert_eq!(linear.xy1, Alignment::from_uv(0.0, 0.0)); + assert_eq!(linear.xy2, Alignment::from_uv(1.0, 1.0)); + assert_eq!(linear.opacity, 0.8); + assert_eq!(linear.stops.len(), 2); + assert_eq!(linear.stops[0].offset, 0.0); + assert_eq!(linear.stops[1].offset, 1.0); + + let Paint::Image(image) = &card.fills[2] else { + panic!("third paint must be image"); + }; + assert_eq!(image.image, ResourceRef::Rid("./noise.png".into())); + assert_eq!(image.fit, ImagePaintFit::Fit(BoxFit::Cover)); + assert_eq!(image.opacity, 0.15); +} + +#[test] +fn canonical_writer_preserves_rich_fill_order_and_uses_the_family_gradient() { + let doc = n0_xml::parse(THREE_LAYER).unwrap(); + let printed = n0_xml::print(&doc).expect("rich paints print"); + let solid = printed.find(""), "{printed}"); + assert!(!printed.contains(""##, + ) + .unwrap(); + let printed = n0_xml::print(&shorthand).unwrap(); + assert!(printed.contains("fill=\"#FFFFFF\""), "{printed}"); + assert!(!printed.contains(""), "{printed}"); + + let structured = n0_xml::parse( + r##""##, + ) + .unwrap(); + let structured_printed = n0_xml::print(&structured).unwrap(); + assert!( + structured_printed.contains("fill=\"#AABBCC\""), + "{structured_printed}" + ); + assert!( + !structured_printed.contains(""), + "{structured_printed}" + ); + + let defaulted = n0_xml::parse( + r#""#, + ) + .unwrap(); + assert_eq!( + defaulted.get(named(&defaulted, "default")).fills, + Paints::solid(Color::BLACK) + ); + let default_printed = n0_xml::print(&defaulted).unwrap(); + assert!(!default_printed.contains(" fill="), "{default_printed}"); + assert!(!default_printed.contains(""#, + ) + .unwrap(); + assert!(empty.get(named(&empty, "empty")).fills.is_empty()); + let empty_printed = n0_xml::print(&empty).unwrap(); + assert!(empty_printed.contains(""), "{empty_printed}"); + + let empty_container = n0_xml::parse( + r#""#, + ) + .unwrap(); + let empty_container_printed = n0_xml::print(&empty_container).unwrap(); + assert!( + !empty_container_printed.contains(""##, + ) + .unwrap(); + let translucent_printed = n0_xml::print(&translucent).unwrap(); + assert!( + translucent_printed.contains(""), + "{translucent_printed}" + ); + assert!( + translucent_printed.contains("opacity=\"0.5019608\""), + "{translucent_printed}" + ); + + let conflict = n0_xml::parse( + r##""##, + ) + .unwrap_err(); + assert!(conflict + .to_string() + .contains("both the `fill` attribute and ")); +} + +#[test] +fn all_gradient_variants_and_common_properties_round_trip() { + let source = r##" + + + + + + +"##; + let doc = n0_xml::parse(source).unwrap(); + let fills = &doc.get(named(&doc, "painted")).fills; + assert!(matches!(fills[0], Paint::Solid(_))); + assert!(matches!(fills[1], Paint::RadialGradient(_))); + assert!(matches!(fills[2], Paint::SweepGradient(_))); + assert!(matches!(fills[3], Paint::DiamondGradient(_))); + let Paint::Solid(solid) = &fills[0] else { + unreachable!() + }; + assert!(!solid.active); + assert_eq!(solid.color.alpha(), 128, "solid opacity quantizes to RGBA8"); + assert_eq!(solid.blend_mode, BlendMode::Multiply); + + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains("opacity=\"0.5019608\""), "{printed}"); + assert!(printed.contains("visible=\"false\""), "{printed}"); + assert!(printed.contains("tile-mode=\"mirror\""), "{printed}"); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn enumerated_paint_values_match_the_model_vocabulary() { + for mode in [ + "normal", + "multiply", + "screen", + "overlay", + "darken", + "lighten", + "color-dodge", + "color-burn", + "hard-light", + "soft-light", + "difference", + "exclusion", + "hue", + "saturation", + "color", + "luminosity", + ] { + let source = format!( + "" + ); + let doc = n0_xml::parse(&source).unwrap(); + let Paint::Solid(solid) = &doc.get(named(&doc, "r")).fills[0] else { + unreachable!() + }; + assert_eq!(solid.blend_mode.as_str(), mode); + } + + for fit in ["cover", "contain", "fill", "none"] { + let source = format!( + "" + ); + let doc = n0_xml::parse(&source).unwrap(); + let Paint::Image(image) = &doc.get(named(&doc, "r")).fills[0] else { + unreachable!() + }; + let ImagePaintFit::Fit(actual) = image.fit else { + unreachable!() + }; + assert_eq!(actual.as_str(), fit); + } + + for tile_mode in ["clamp", "repeated", "mirror", "decal"] { + let source = format!( + "" + ); + let doc = n0_xml::parse(&source).unwrap(); + let Paint::LinearGradient(gradient) = &doc.get(named(&doc, "r")).fills[0] else { + unreachable!() + }; + assert_eq!(gradient.tile_mode.as_str(), tile_mode); + } +} + +#[test] +fn text_content_is_exact_around_a_structural_fill_child() { + let source = " hello\nworld "; + let doc = n0_xml::parse(source).unwrap(); + let label = doc.get(named(&doc, "label")); + let Payload::Text { content, .. } = &label.payload else { + panic!("text payload") + }; + assert_eq!(content, " hello\nworld "); + let printed = n0_xml::print(&doc).unwrap(); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn paint_context_and_structure_errors_are_targeted() { + let cases = [ + ( + r##""##, + "direct child of or ", + ), + ( + r#""#, + "scene is not supported", + ), + ( + r##""##, + "direct child of a gradient", + ), + ( + r#""#, + "not valid on ", + ), + ( + r#""#, + "duplicate ", + ), + ( + r#""#, + "nested ", + ), + ( + r#"x"#, + "must appear before", + ), + ]; + for (source, expected) in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected {expected}: {error}" + ); + } +} + +#[test] +fn bad_paint_values_are_rejected_without_coercion() { + let cases = [ + ("", "#RGB or #RRGGBB"), + ("", "must be empty"), + ("", "exactly `true` or `false`"), + ("", "between 0 and 1"), + ("", "layer mode"), + ("", "src must not be empty"), + ("", "must be empty"), + ("", "bad image fit"), + ("", "kind"), + ("", "kind"), + ("", "from"), + ("", "tile-mode"), + ("", "six numbers"), + ("not-a-stop", "character content"), + ("", "only empty "), + ("", "must be empty"), + ("", "requires `offset`"), + ("", "requires `color`"), + ("", "visible"), + ("", "between 0 and 1"), + ("", "at least two stops"), + ("", "farther apart"), + ("", "farther apart"), + ("", "farther apart"), + ("", "overflow after lowering"), + ("", "nondecreasing"), + ("", "between 0 and 1"), + ]; + for (paint, expected) in cases { + let source = format!( + "{paint}" + ); + let error = n0_xml::parse(&source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected {expected}: {error}" + ); + } +} + +#[test] +fn gradient_renderability_boundary_is_enforced_during_source_ingestion() { + let source = |paint: &str| { + format!( + "{paint}" + ) + }; + let stops = ""; + + let singular = source(&format!( + "{stops}" + )); + let error = n0_xml::parse(&singular).unwrap_err(); + assert!( + error.to_string().contains("transform must be invertible"), + "{error}" + ); + + for distance in ["0.0000152587890625", "0.000030517578125"] { + let degenerate = source(&format!( + "{stops}" + )); + let error = n0_xml::parse(°enerate).unwrap_err(); + assert!( + error.to_string().contains("farther apart"), + "distance={distance}: {error}" + ); + } + + let renderable = source(&format!( + "{stops}" + )); + n0_xml::parse(&renderable) + .expect("a linear gradient above Skia's degeneracy threshold remains valid source"); +} + +#[test] +fn linear_gradient_uv_boundary_is_exact_or_explicitly_unrepresentable() { + for (u, v) in [ + (0.0, 0.0), + (0.5, 0.5), + (0.55, 0.125), + (-2.5, 1.75), + (f64::from_bits(1), f64::from_bits(2)), + ] { + let alignment = Alignment::from_uv_f64(u, v); + let canonical = alignment + .try_to_uv() + .expect("values produced by the UV boundary are representable"); + assert_eq!(Alignment::from_uv_f64(canonical.0, canonical.1), alignment); + } + + for alignment in [ + Alignment(0.1, -0.1), + Alignment(0.25, -12345.678), + Alignment(-1.0, 1.0), + Alignment(f32::MAX, -f32::MAX), + ] { + let canonical = alignment + .try_to_uv() + .expect("ordinary and extreme finite alignments are representable"); + assert_eq!(Alignment::from_uv_f64(canonical.0, canonical.1), alignment); + } + + let mut doc = n0_xml::parse( + r##""##, + ) + .unwrap(); + let r = named(&doc, "r"); + let Paint::LinearGradient(gradient) = &mut doc.get_mut(r).fills.as_mut_slice()[0] else { + unreachable!() + }; + gradient.xy1 = Alignment(0.1, 0.25); + gradient.xy2 = Alignment(f32::MAX, -f32::MAX); + let printed = n0_xml::print(&doc).expect("finite f64-representable alignments print exactly"); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); + + let Paint::LinearGradient(gradient) = &mut doc.get_mut(r).fills.as_mut_slice()[0] else { + unreachable!() + }; + gradient.xy1 = Alignment(f32::from_bits(1), gradient.xy1.1); + assert!(gradient.xy1.try_to_uv().is_none()); + assert!(matches!( + n0_xml::print(&doc), + Err(PrintError::InvalidDocument(message)) + if message.contains("from alignment is not representable") + && message.contains("binary64 UV arithmetic") + )); +} + +#[test] +fn writer_refuses_unspellable_gradient_model_state_without_repair() { + let source = r##""##; + + let mut too_few = n0_xml::parse(source).unwrap(); + let r = named(&too_few, "r"); + let Paint::LinearGradient(gradient) = &mut too_few.get_mut(r).fills.as_mut_slice()[0] else { + unreachable!() + }; + gradient.stops.pop(); + assert!(matches!( + n0_xml::print(&too_few), + Err(PrintError::InvalidDocument(message)) if message.contains("at least two stops") + )); + + let mut descending = n0_xml::parse(source).unwrap(); + let r = named(&descending, "r"); + let Paint::LinearGradient(gradient) = &mut descending.get_mut(r).fills.as_mut_slice()[0] else { + unreachable!() + }; + gradient.stops[0].offset = 0.8; + gradient.stops[1].offset = 0.2; + assert!(matches!( + n0_xml::print(&descending), + Err(PrintError::InvalidDocument(message)) if message.contains("nondecreasing") + )); + + let mut coincident = n0_xml::parse(source).unwrap(); + let r = named(&coincident, "r"); + let Paint::LinearGradient(gradient) = &mut coincident.get_mut(r).fills.as_mut_slice()[0] else { + unreachable!() + }; + gradient.xy2 = gradient.xy1; + assert!(matches!( + n0_xml::print(&coincident), + Err(PrintError::InvalidDocument(message)) if message.contains("farther apart") + )); +} + +#[test] +fn line_is_stroke_only_and_rejects_both_fill_forms() { + for source in [ + r##""##, + r##""##, + ] { + let error = n0_xml::parse(source).unwrap_err(); + assert!(error.to_string().contains("fill"), "{error}"); + assert!(error.to_string().contains("line"), "{error}"); + } +} + +#[test] +fn retired_paint_spellings_are_rejected_with_canonical_directions() { + let cases = [ + ( + r##""##, + "", + ), + ( + r##""##, + "use "##, + "use "##, + "use "##, + "use "##, + "unknown paint element ", + ), + ( + r##""##, + "unknown paint element ", + ), + ( + r##""##, + "kind", + ), + ( + r##""##, + "", + ), + ( + r##""##, + "", + ), + ]; + for (source, expected) in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}`" + ); + } +} + +#[test] +fn frozen_textir_is_singleton_solid_and_rich_state_fails_writers() { + let historical = n0_model::textir::parse( + r##""##, + ) + .unwrap(); + let printed = n0_model::textir::try_print(&historical).unwrap(); + assert!(printed.contains("fill=\"#000000\"")); + assert_eq!(historical, n0_model::textir::parse(&printed).unwrap()); + + for (legacy, canonical) in [ + ("112233", "#112233"), + ("#445566", "#445566"), + ("##778899", "#778899"), + ] { + let source = format!( + r#""# + ); + let doc = n0_model::textir::parse(&source).unwrap(); + assert!( + n0_model::textir::try_print(&doc) + .unwrap() + .contains(&format!(r#"fill="{canonical}""#)), + "legacy color {legacy}" + ); + } + + let grida_short = n0_xml::parse( + r##""##, + ) + .unwrap(); + assert!(n0_xml::print(&grida_short) + .unwrap() + .contains("fill=\"#AABBCC\"")); + assert!(n0_model::textir::parse( + r##""## + ) + .is_err()); + + let rich = n0_xml::parse(THREE_LAYER).unwrap(); + assert!(n0_model::textir::try_print(&rich).is_err()); + + let mut unsupported_image = rich.clone(); + let card = named(&unsupported_image, "card"); + let Paint::Image(image) = &mut unsupported_image.get_mut(card).fills.as_mut_slice()[2] else { + panic!("image paint") + }; + image.quarter_turns = 1; + assert!(matches!( + n0_xml::print(&unsupported_image), + Err(PrintError::InvalidDocument(message)) if message.contains("quarter-turns") + )); +} diff --git a/crates/n0-model/tests/n0_xml_paths.rs b/crates/n0-model/tests/n0_xml_paths.rs new file mode 100644 index 00000000..7470924e --- /dev/null +++ b/crates/n0-model/tests/n0_xml_paths.rs @@ -0,0 +1,650 @@ +use n0_model::math::RectF; +use n0_model::model::{ + AxisBinding, Color, FillRule, Paints, Payload, ShapeDesc, SizeIntent, StrokeAlign, StrokeCap, + StrokeJoin, +}; +use n0_model::n0_xml; +use n0_model::path::{self, PathCommand}; +use n0_model::resolve::{resolve, ResolveOptions}; +use n0_model::svgout::{self, SvgOptions}; +use n0_model::textir; + +fn source(path: &str) -> String { + format!( + "{path}" + ) +} + +fn only_path(document: &n0_model::model::Document) -> n0_model::model::NodeId { + let render_root = document.get(document.root).children[0]; + document.get(render_root).children[0] +} + +#[test] +fn path_is_a_boxed_unit_reference_shape_with_one_shared_artifact() { + let d = "M 0 1 L .5 0 L 1 1 Z"; + let document = n0_xml::parse(&source(&format!( + "" + ))) + .unwrap(); + let id = only_path(&document); + let node = document.get(id); + let Payload::Shape { + desc: ShapeDesc::Path(artifact), + } = &node.payload + else { + panic!("expected path shape"); + }; + assert_eq!(artifact.d(), d); + assert_eq!(artifact.fill_rule(), FillRule::NonZero); + assert!(artifact.geometry().all_contours_closed); + assert_eq!( + artifact.geometry().unit_bounds, + RectF { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0 + } + ); + assert_eq!(node.fills, Paints::solid(Color::BLACK)); + + let resolved = resolve(&document, &ResolveOptions::default()); + let resolved_path = resolved.resolved_path_of(id); + assert!(matches!( + resolved_path.commands[0], + PathCommand::MoveTo { x: 0.0, y: 100.0 } + )); + assert_eq!( + resolved.aabb_of(id), + RectF { + x: 10.0, + y: 20.0, + w: 200.0, + h: 100.0, + } + ); + assert!(resolved.resolved_path_opt(document.root).is_none()); +} + +#[test] +fn tight_path_geometry_not_the_layout_box_starts_visual_bounds() { + let document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let id = only_path(&document); + let resolved = resolve(&document, &ResolveOptions::default()); + assert_eq!(resolved.xywh(id), (10.0, 20.0, 200.0, 100.0)); + assert_eq!( + resolved.aabb_of(id), + RectF { + x: 60.0, + y: 45.0, + w: 100.0, + h: 50.0, + } + ); +} + +#[test] +fn path_miter_strokes_expand_bounds_conservatively() { + let document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let id = only_path(&document); + let bounds = resolve(&document, &ResolveOptions::default()).aabb_of(id); + assert_eq!( + bounds, + RectF { + x: 17.0, + y: 17.0, + w: 66.0, + h: 66.0, + } + ); +} + +#[test] +fn evenodd_and_source_spelling_round_trip_canonically() { + let d = "m 0 0 l 1 0 0 1 -1 0 z"; + let document = n0_xml::parse(&source(&format!( + "" + ))) + .unwrap(); + let printed = n0_xml::print(&document).unwrap(); + assert!(printed.contains(&format!("d=\"{d}\""))); + assert!(printed.contains("fill-rule=\"evenodd\"")); + let reparsed = n0_xml::parse(&printed).unwrap(); + let Payload::Shape { + desc: ShapeDesc::Path(path), + } = &reparsed.get(only_path(&reparsed)).payload + else { + panic!("expected path"); + }; + assert_eq!(path.fill_rule(), FillRule::EvenOdd); + assert_eq!(path.d(), d); + assert!(textir::try_print(&document) + .unwrap_err() + .to_string() + .contains("the historical TextIr dialect cannot represent")); +} + +#[test] +fn equivalent_path_spellings_share_visual_identity() { + let absolute = path::analyze("M 0 0 L 1 0 L 1 1 Z", FillRule::NonZero).unwrap(); + let relative = path::analyze("m0 0h1v1z", FillRule::NonZero).unwrap(); + assert_ne!(absolute.d(), relative.d()); + assert_ne!( + absolute, relative, + "source-tier equality retains authored d" + ); + assert!(absolute.same_visual_geometry(&relative)); + + let evenodd = path::analyze("m0 0h1v1z", FillRule::EvenOdd).unwrap(); + assert!(!absolute.same_visual_geometry(&evenodd)); + + let arc_absolute = path::analyze("M .5 0 A .5 .5 0 0 1 1 .5", FillRule::NonZero).unwrap(); + let arc_relative = path::analyze("m .5 0 a .5 .5 360 0 1 .5 .5", FillRule::NonZero).unwrap(); + assert_ne!(arc_absolute.d(), arc_relative.d()); + assert_ne!(arc_absolute, arc_relative); + assert!(arc_absolute.same_visual_geometry(&arc_relative)); +} + +#[test] +fn curves_use_realized_extrema_and_arcs_become_bounded_rational_conics() { + // The quadratic control point is outside the unit box, but the realized + // curve only touches y=0 and is therefore valid. + let quadratic = path::analyze("M 0 1 Q .5 -1 1 1", FillRule::NonZero).unwrap(); + assert_eq!( + quadratic.geometry().unit_bounds, + RectF { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0, + } + ); + let overshoot = path::analyze("M 0 1 Q .5 -1.1 1 1", FillRule::NonZero) + .unwrap_err() + .to_string(); + assert!(overshoot.contains("unit reference box"), "{overshoot}"); + + let circle = path::analyze( + "M .5 0 A .5 .5 0 0 1 1 .5 A .5 .5 0 0 1 .5 1 A .5 .5 0 0 1 0 .5 A .5 .5 0 0 1 .5 0 Z", + FillRule::NonZero, + ) + .unwrap(); + let conics: Vec<_> = circle + .geometry() + .commands + .iter() + .filter_map(|command| match command { + PathCommand::ConicTo { + x1, + y1, + x, + y, + weight, + } => Some((*x1, *y1, *x, *y, *weight)), + _ => None, + }) + .collect(); + assert_eq!(conics.len(), 4); + assert_eq!(conics[0].0, 1.0); + assert_eq!(conics[0].1, 0.0); + assert_eq!(conics[0].2, 1.0); + assert_eq!(conics[0].3, 0.5); + for (_, _, _, _, weight) in conics { + assert!((weight - std::f32::consts::FRAC_1_SQRT_2).abs() < 1.0e-7); + } + assert!(circle.geometry().all_contours_closed); + assert_eq!( + circle.geometry().unit_bounds, + RectF { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0 + } + ); + for rotation in [1, 89, 91, 180, 270, 359] { + let rotated_circle = path::analyze( + format!( + "M .5 0 A .5 .5 {rotation} 0 1 1 .5 A .5 .5 {rotation} 0 1 .5 1 A .5 .5 {rotation} 0 1 0 .5 A .5 .5 {rotation} 0 1 .5 0 Z" + ), + FillRule::NonZero, + ) + .unwrap(); + assert!( + rotated_circle.same_visual_geometry(&circle), + "circle rotation {rotation}" + ); + } + + let rotated_ellipse = path::analyze( + "M .5 0 A .5 .25 90 0 1 .75 .5 A .5 .25 90 0 1 .5 1 A .5 .25 90 0 1 .25 .5 A .5 .25 90 0 1 .5 0 Z", + FillRule::NonZero, + ) + .unwrap(); + assert_eq!( + rotated_ellipse.geometry().unit_bounds, + RectF { + x: 0.25, + y: 0.0, + w: 0.5, + h: 1.0, + } + ); + assert_eq!( + rotated_ellipse + .geometry() + .commands + .iter() + .filter(|command| matches!(command, PathCommand::ConicTo { .. })) + .count(), + 4 + ); + + let tiny = path::analyze( + "M .5 .5 A .000001 .000001 0 0 1 .500001 .500001", + FillRule::NonZero, + ) + .unwrap(); + assert!(matches!( + tiny.geometry().commands[1], + PathCommand::ConicTo { .. } + )); + + let extreme_large_arc = path::analyze("M 0 0 A 1e-20 1e-20 37 1 1 1e-45 0", FillRule::NonZero) + .unwrap_err() + .to_string(); + assert!( + extreme_large_arc.contains("unit reference box"), + "{extreme_large_arc}" + ); +} + +#[test] +fn path_reports_focused_source_errors() { + let cases = [ + ("", "requires `d`"), + ( + "", + "must not be empty", + ), + ( + "", + "invalid SVG path data", + ), + ( + "", + "unit reference box", + ), + ( + "", + "unit reference box", + ), + ( + "", + "must be `nonzero` or `evenodd`", + ), + ( + "", + "duplicate `fill-rule`", + ), + ( + "", + "requires both axes", + ), + ]; + for (path, expected) in cases { + let error = n0_xml::parse(&source(path)).unwrap_err().to_string(); + assert!( + error.contains(expected), + "{error:?} did not contain {expected:?}" + ); + } +} + +#[test] +fn move_only_contours_and_coincident_arcs_do_not_invent_geometry_or_bounds() { + let path = path::analyze("M 1 1 M 0 0 L 1 1", FillRule::NonZero).unwrap(); + assert_eq!( + path.geometry().unit_bounds, + RectF { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0 + } + ); + + let error = path::analyze("M .5 .5 A .2 .2 0 0 1 .5 .5", FillRule::NonZero) + .unwrap_err() + .to_string(); + assert!(error.contains("at least one drawing segment"), "{error}"); +} + +#[test] +fn open_and_closed_path_stroke_contracts_are_explicit() { + let open_error = n0_xml::parse(&source( + "", + )) + .unwrap_err() + .to_string(); + assert!(open_error.contains("every drawable contour is explicitly closed")); + + let document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let stroke = &document.get(only_path(&document)).strokes[0]; + assert_eq!(stroke.align, StrokeAlign::Outside); + assert_eq!(stroke.cap, StrokeCap::Round); + assert_eq!(stroke.join, StrokeJoin::Bevel); + assert_eq!(stroke.miter_limit, 7.0); + assert_eq!(stroke.dash_array.as_deref(), Some(&[2.0, 1.0][..])); + + let per_side = n0_xml::parse(&source( + "", + )) + .unwrap_err() + .to_string(); + assert!(per_side.contains("four-value stroke width is valid only")); +} + +#[test] +fn path_accepts_render_children_in_its_declared_local_box() { + let document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let path = only_path(&document); + let child = document.get(path).children[0]; + let resolved = resolve(&document, &ResolveOptions::default()); + assert_eq!(resolved.xywh(path), (0.0, 0.0, 200.0, 100.0)); + assert_eq!(resolved.xywh(child), (50.0, 20.0, 30.0, 10.0)); + assert!(matches!( + resolved.resolved_path_of(path).commands.last(), + Some(PathCommand::Close) + )); +} + +#[test] +fn svg_snapshot_maps_the_unit_path_through_its_resolved_box() { + let document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let resolved = resolve(&document, &ResolveOptions::default()); + let svg = svgout::render( + &document, + &resolved, + &SvgOptions { + show_aabb: false, + width: 400.0, + height: 300.0, + }, + ) + .unwrap(); + assert!(svg.contains( + "" + )); + + let arc_document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let arc_svg = svgout::render( + &arc_document, + &resolve(&arc_document, &ResolveOptions::default()), + &SvgOptions { + show_aabb: false, + width: 400.0, + height: 300.0, + }, + ) + .unwrap(); + assert!(arc_svg.contains("d=\"M .5 0 A .5 .5 0 0 1 1 .5\"")); + + let mut imported_document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let imported_id = only_path(&imported_document); + let imported = + path::analyze_in_reference_box("M 0 0 H 200 V 100 Z", FillRule::NonZero, 200.0, 100.0) + .unwrap(); + let Payload::Shape { + desc: ShapeDesc::Path(path), + } = &mut imported_document.get_mut(imported_id).payload + else { + panic!("fixture contains a path") + }; + *path = imported; + let imported_svg = svgout::render( + &imported_document, + &resolve(&imported_document, &ResolveOptions::default()), + &SvgOptions { + show_aabb: false, + width: 400.0, + height: 300.0, + }, + ) + .unwrap(); + assert!(imported_svg + .contains("", + )) + .unwrap(); + let id = only_path(&base); + let rejected = |mut document: n0_model::model::Document, + mutate: fn(&mut n0_model::model::Header), + expected: &str| { + mutate(&mut document.get_mut(id).header); + let error = n0_xml::print(&document).unwrap_err().to_string(); + assert!( + error.contains(expected), + "{error:?} did not contain {expected:?}" + ); + }; + + rejected( + base.clone(), + |header| { + header.width = SizeIntent::Auto; + header.height = SizeIntent::Auto; + }, + "requires both axes", + ); + rejected( + base.clone(), + |header| header.aspect_ratio = Some((2.0, 1.0)), + "requires both axes", + ); + rejected( + base.clone(), + |header| { + header.x = AxisBinding::Span { + start: 0.0, + end: 0.0, + } + }, + "span x binding", + ); + rejected( + base.clone(), + |header| header.min_width = Some(-1.0), + "min-width must be a finite non-negative", + ); + rejected( + base.clone(), + |header| header.aspect_ratio = Some((0.0, 1.0)), + "aspect-ratio terms", + ); + rejected( + base.clone(), + |header| header.x = AxisBinding::start(f32::NAN), + "x binding must contain finite", + ); + rejected( + base, + |header| header.width = SizeIntent::Fixed(-1.0), + "width must be a finite non-negative", + ); +} + +#[test] +fn programmatic_open_path_alignment_fails_closed_for_paint_and_bounds() { + let mut document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let id = only_path(&document); + document.get_mut(id).strokes[0].align = StrokeAlign::Inside; + let node = document.get(id); + assert!(!node.strokes[0].renderable_for(&node.payload, node.corner_smoothing)); + assert_eq!( + resolve(&document, &ResolveOptions::default()).aabb_of(id), + RectF { + x: 25.0, + y: 25.0, + w: 50.0, + h: 50.0, + } + ); + assert!(n0_xml::print(&document) + .unwrap_err() + .to_string() + .contains("every drawable contour is explicitly closed")); +} + +#[test] +fn unit_and_huge_mapped_bounds_conservatively_contain_stored_commands() { + let line = path::analyze( + "M 0.45802047848701477 0 L 0.9617093205451965 0", + FillRule::NonZero, + ) + .unwrap(); + let PathCommand::LineTo { x: unit_end, .. } = line.geometry().commands[1] else { + panic!("expected line endpoint"); + }; + assert!(line.geometry().unit_bounds.x + line.geometry().unit_bounds.w >= unit_end); + + let mapped = path::materialize( + line.geometry(), + line.fill_rule(), + 30_270_271_994_254_590_000.0_f32, + 1.0, + ) + .unwrap(); + let PathCommand::LineTo { x: mapped_end, .. } = mapped.commands[1] else { + panic!("expected mapped line endpoint"); + }; + assert!(mapped.local_bounds.x + mapped.local_bounds.w >= mapped_end); + + let circle = path::analyze( + "M .5 0 A .5 .5 0 0 1 1 .5 A .5 .5 0 0 1 .5 1 A .5 .5 0 0 1 0 .5 A .5 .5 0 0 1 .5 0 Z", + FillRule::NonZero, + ) + .unwrap(); + let mapped = path::materialize( + circle.geometry(), + circle.fill_rule(), + 19_384_729_239_552.0, + 7_193_847_234_560.0, + ) + .unwrap(); + let rotated_circle = path::analyze( + "M .5 0 A .5 .5 359 0 1 1 .5 A .5 .5 359 0 1 .5 1 A .5 .5 359 0 1 0 .5 A .5 .5 359 0 1 .5 0 Z", + FillRule::NonZero, + ) + .unwrap(); + let rotated_mapped = path::materialize( + rotated_circle.geometry(), + rotated_circle.fill_rule(), + 19_384_729_239_552.0, + 7_193_847_234_560.0, + ) + .unwrap(); + assert_eq!(rotated_mapped, mapped); + let bounds = mapped.local_bounds; + let left = f64::from(bounds.x); + let top = f64::from(bounds.y); + let right = f64::from(bounds.x + bounds.w); + let bottom = f64::from(bounds.y + bounds.h); + let mut current = (0.0_f32, 0.0_f32); + for command in mapped.commands.iter() { + match *command { + PathCommand::MoveTo { x, y } | PathCommand::LineTo { x, y } => { + current = (x, y); + } + PathCommand::ConicTo { + x1, + y1, + x, + y, + weight, + } => { + for sample in 0..=1024 { + let t = sample as f64 / 1024.0; + let one_minus_t = 1.0 - t; + let a = one_minus_t * one_minus_t; + let b = 2.0 * f64::from(weight) * one_minus_t * t; + let c = t * t; + let denominator = a + b + c; + let px = (a * f64::from(current.0) + b * f64::from(x1) + c * f64::from(x)) + / denominator; + let py = (a * f64::from(current.1) + b * f64::from(y1) + c * f64::from(y)) + / denominator; + assert!(px >= left && px <= right, "x={px} outside {left}..{right}"); + assert!(py >= top && py <= bottom, "y={py} outside {top}..{bottom}"); + } + current = (x, y); + } + PathCommand::Close => {} + PathCommand::QuadTo { .. } | PathCommand::CubicTo { .. } => { + unreachable!("circle arc lowers only to conics") + } + } + } +} + +#[test] +fn resolved_control_overflow_fails_closed_without_losing_children() { + let mut document = n0_xml::parse(&source( + "", + )) + .unwrap(); + let id = only_path(&document); + let child = document.get(id).children[0]; + document.get_mut(id).header.height = SizeIntent::Fixed(f32::MAX); + + let resolved = resolve(&document, &ResolveOptions::default()); + assert!(resolved.resolved_path_opt(id).is_none()); + assert!(resolved.reports.iter().any(|report| matches!( + report, + n0_model::resolve::Report::ErrorByRule { + node, + field: "path", + rule: "resolved path coordinates exceed finite f32 geometry", + } if *node == id + ))); + assert_eq!( + resolved.aabb_of(child), + RectF { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0, + } + ); + assert_eq!(resolved.aabb_of(id), resolved.aabb_of(child)); +} diff --git a/crates/n0-model/tests/n0_xml_properties.rs b/crates/n0-model/tests/n0_xml_properties.rs new file mode 100644 index 00000000..fbbaa15e --- /dev/null +++ b/crates/n0-model/tests/n0_xml_properties.rs @@ -0,0 +1,366 @@ +//! Positive Draft 0 property coverage that complements `n0_xml_suite`. +//! +//! That suite already proves `name`, fixed `width`/`height`, all four size +//! constraints, `aspect-ratio`, bare start bindings, and `flip-y`. This file +//! covers the accepted value forms that otherwise lacked both direct model +//! assertions and a canonical parse -> print -> parse fixpoint. + +use n0_model::model::*; +use n0_model::n0_xml; + +fn canonical(source: &str) -> (Document, String) { + let doc = n0_xml::parse(source).expect("Draft 0 source parses"); + let printed = n0_xml::print(&doc).expect("parsed Draft 0 source prints"); + let reparsed = n0_xml::parse(&printed).expect("canonical source reparses"); + assert_eq!(doc, reparsed, "semantic round-trip\n---\n{printed}"); + assert_eq!( + printed, + n0_xml::print(&reparsed).expect("canonical source reprints"), + "writer fixpoint" + ); + (doc, printed) +} + +fn authored_root(doc: &Document) -> NodeId { + doc.get(doc.root).children[0] +} + +fn frame_layout(doc: &Document, id: NodeId) -> LayoutBehavior { + match doc.get(id).payload { + Payload::Frame { layout, .. } => layout, + ref payload => panic!("expected container, got {payload:?}"), + } +} + +#[test] +fn binding_forms_auto_sizes_and_common_visual_attributes_materialize() { + let source = r#" + + + +"#; + let (doc, printed) = canonical(source); + let root = authored_root(&doc); + let root_node = doc.get(root); + assert_eq!(root_node.header.width, SizeIntent::Auto); + assert_eq!(root_node.header.height, SizeIntent::Auto); + assert!(matches!( + root_node.payload, + Payload::Frame { + clips_content: true, + .. + } + )); + + let [start, end, center, span_x, span_y, text, unclipped] = root_node.children.as_slice() + else { + panic!("expected all property probes") + }; + + let start = doc.get(*start); + assert_eq!(start.header.x, AxisBinding::start(-5.0)); + assert_eq!(start.header.y, AxisBinding::start(7.0)); + assert_eq!(start.header.rotation, -12.5); + assert!(start.header.flip_x); + assert!(!start.header.flip_y); + assert_eq!(start.header.opacity, 0.25); + assert!(!start.header.active); + + let end = doc.get(*end); + assert_eq!(end.header.x, AxisBinding::end(8.0)); + assert_eq!(end.header.y, AxisBinding::end(-2.0)); + + let center = doc.get(*center); + assert_eq!(center.header.x, AxisBinding::center(0.0)); + assert_eq!(center.header.y, AxisBinding::center(3.0)); + + let span_x = doc.get(*span_x); + assert_eq!( + span_x.header.x, + AxisBinding::Span { + start: 1.0, + end: 2.0 + } + ); + assert_eq!(span_x.header.y, AxisBinding::start(4.0)); + assert_eq!(span_x.header.width, SizeIntent::Auto); + + let span_y = doc.get(*span_y); + assert_eq!(span_y.header.x, AxisBinding::start(5.0)); + assert_eq!( + span_y.header.y, + AxisBinding::Span { + start: 3.0, + end: 4.0 + } + ); + assert_eq!(span_y.header.height, SizeIntent::Auto); + + let text = doc.get(*text); + assert_eq!(text.header.width, SizeIntent::Auto); + assert_eq!(text.header.height, SizeIntent::Auto); + assert_eq!(text.header.opacity, 1.0); + assert!(text.header.active); + assert!(matches!( + text.payload, + Payload::Text { + font_size: 21.0, + .. + } + )); + + assert!(matches!( + doc.get(*unclipped).payload, + Payload::Frame { + clips_content: false, + .. + } + )); + + assert!(printed.contains("")); + assert!(printed.contains("x=\"-5\" y=\"7\""), "{printed}"); + assert!(!printed.contains("x=\"start "), "{printed}"); + assert!(printed.contains("x=\"end 8\" y=\"end -2\""), "{printed}"); + assert!(printed.contains("x=\"center\" y=\"center 3\""), "{printed}"); + assert!(printed.contains("x=\"span 1 2\" y=\"4\""), "{printed}"); + assert!(printed.contains("x=\"5\" y=\"span 3 4\""), "{printed}"); + assert!(printed.contains("rotation=\"-12.5\""), "{printed}"); + assert!(printed.contains("flip-x=\"true\""), "{printed}"); + assert!(!printed.contains("flip-y="), "{printed}"); + assert!(printed.contains("opacity=\"0.25\""), "{printed}"); + assert!(printed.contains("hidden=\"true\""), "{printed}"); + assert!(!printed.contains("hidden=\"false\""), "{printed}"); + assert!(!printed.contains("clips=\"false\""), "{printed}"); + assert!( + printed.contains("x"), + "{printed}" + ); +} + +#[test] +fn every_lens_operation_materializes_in_source_order() { + let source = r#" + + + + + + + +"#; + let (doc, printed) = canonical(source); + let root = authored_root(&doc); + let lens = doc.get(root).children[0]; + assert_eq!( + doc.get(lens).payload, + Payload::Lens { + ops: vec![ + LensOp::Translate { x: 1.0, y: -2.0 }, + LensOp::Rotate { deg: 45.0 }, + LensOp::Scale { x: 2.0, y: 2.0 }, + LensOp::Scale { x: 3.0, y: 4.0 }, + LensOp::Skew { + x_deg: 5.0, + y_deg: 0.0 + }, + LensOp::Skew { + x_deg: 0.0, + y_deg: -6.0 + }, + LensOp::Skew { + x_deg: 7.0, + y_deg: 8.0 + }, + LensOp::Matrix { + m: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + }, + ] + } + ); + assert!( + printed.contains( + "ops=\"translate(1,-2) rotate(45) scale(2) scale(3,4) skew-x(5) skew-y(-6) skew(7,8) matrix(1,2,3,4,5,6)\"" + ), + "{printed}" + ); +} + +#[test] +fn layout_and_flex_enum_variants_materialize_and_canonicalize() { + let (none, printed) = + canonical(r#""#); + let none_layout = frame_layout(&none, authored_root(&none)); + assert_eq!(none_layout.mode, LayoutMode::None); + assert_eq!(none_layout.padding, EdgeInsets::all(3.0)); + assert!(!printed.contains("layout="), "{printed}"); + assert!(printed.contains("padding=\"3\""), "{printed}"); + + for (value, expected, canonical_attr) in [ + ("row", Direction::Row, None), + ("column", Direction::Column, Some("direction=\"column\"")), + ] { + let source = format!( + "" + ); + let (doc, printed) = canonical(&source); + let layout = frame_layout(&doc, authored_root(&doc)); + assert_eq!(layout.mode, LayoutMode::Flex); + assert_eq!(layout.direction, expected); + match canonical_attr { + Some(attr) => assert!(printed.contains(attr), "{printed}"), + None => assert!(!printed.contains("direction="), "{printed}"), + } + } + + for (value, expected) in [("false", false), ("true", true)] { + let source = + format!(""); + let (doc, printed) = canonical(&source); + assert_eq!(frame_layout(&doc, authored_root(&doc)).wrap, expected); + assert_eq!(printed.contains("wrap=\"true\""), expected, "{printed}"); + assert!(!printed.contains("wrap=\"false\""), "{printed}"); + } + + for (value, expected, canonical_attr) in [ + ("start", MainAlign::Start, None), + ("center", MainAlign::Center, Some("main=\"center\"")), + ("end", MainAlign::End, Some("main=\"end\"")), + ( + "space-between", + MainAlign::SpaceBetween, + Some("main=\"space-between\""), + ), + ( + "space-around", + MainAlign::SpaceAround, + Some("main=\"space-around\""), + ), + ( + "space-evenly", + MainAlign::SpaceEvenly, + Some("main=\"space-evenly\""), + ), + ] { + let source = + format!(""); + let (doc, printed) = canonical(&source); + assert_eq!(frame_layout(&doc, authored_root(&doc)).main_align, expected); + match canonical_attr { + Some(attr) => assert!(printed.contains(attr), "{printed}"), + None => assert!(!printed.contains(" main="), "{printed}"), + } + } + + for (value, expected, canonical_attr) in [ + ("start", CrossAlign::Start, None), + ("center", CrossAlign::Center, Some("cross=\"center\"")), + ("end", CrossAlign::End, Some("cross=\"end\"")), + ("stretch", CrossAlign::Stretch, Some("cross=\"stretch\"")), + ] { + let source = + format!(""); + let (doc, printed) = canonical(&source); + assert_eq!( + frame_layout(&doc, authored_root(&doc)).cross_align, + expected + ); + match canonical_attr { + Some(attr) => assert!(printed.contains(attr), "{printed}"), + None => assert!(!printed.contains(" cross="), "{printed}"), + } + } +} + +#[test] +fn gap_and_padding_grammars_materialize_and_normalize() { + let cases = [ + ("7", "8", (7.0, 7.0), EdgeInsets::all(8.0), "7", "8"), + ( + "4 9", + "1 2 3 4", + (4.0, 9.0), + EdgeInsets { + top: 1.0, + right: 2.0, + bottom: 3.0, + left: 4.0, + }, + "4 9", + "1 2 3 4", + ), + ("6 6", "5 5 5 5", (6.0, 6.0), EdgeInsets::all(5.0), "6", "5"), + ]; + + for (gap, padding, expected_gap, expected_padding, canonical_gap, canonical_padding) in cases { + let source = format!( + "" + ); + let (doc, printed) = canonical(&source); + let layout = frame_layout(&doc, authored_root(&doc)); + assert_eq!((layout.gap_main, layout.gap_cross), expected_gap); + assert_eq!(layout.padding, expected_padding); + assert!( + printed.contains(&format!("gap=\"{canonical_gap}\"")), + "{printed}" + ); + assert!( + printed.contains(&format!("padding=\"{canonical_padding}\"")), + "{printed}" + ); + } +} + +#[test] +fn flex_child_flow_grow_and_all_align_variants_materialize() { + let source = r#" + + + + + + + + + +"#; + let (doc, printed) = canonical(source); + let root = authored_root(&doc); + let children = &doc.get(root).children; + assert_eq!(children.len(), 5); + + for (&id, align) in children[..4].iter().zip([ + SelfAlign::Start, + SelfAlign::Center, + SelfAlign::End, + SelfAlign::Stretch, + ]) { + let header = &doc.get(id).header; + assert_eq!(header.flow, Flow::InFlow); + assert_eq!(header.self_align, align); + } + assert_eq!(doc.get(children[0]).header.grow, 0.0); + assert_eq!(doc.get(children[1]).header.grow, 2.0); + + let absolute = &doc.get(children[4]).header; + assert_eq!(absolute.flow, Flow::Absolute); + assert_eq!(absolute.x, AxisBinding::end(2.0)); + assert_eq!(absolute.y, AxisBinding::center(0.0)); + assert_eq!(absolute.grow, 0.0); + assert_eq!(absolute.self_align, SelfAlign::Auto); + + assert!(!printed.contains("flow=\"in\""), "{printed}"); + assert!(printed.contains("flow=\"absolute\""), "{printed}"); + assert!(!printed.contains("grow=\"0\""), "{printed}"); + assert!(printed.contains("grow=\"2\""), "{printed}"); + for value in ["start", "center", "end", "stretch"] { + assert!(printed.contains(&format!("align=\"{value}\"")), "{printed}"); + } +} diff --git a/crates/n0-model/tests/n0_xml_source.rs b/crates/n0-model/tests/n0_xml_source.rs new file mode 100644 index 00000000..d14397a8 --- /dev/null +++ b/crates/n0-model/tests/n0_xml_source.rs @@ -0,0 +1,572 @@ +//! Producer contract for retained Draft 1 n0 XML source programs. + +use n0_model::model::{AxisBinding, Paint, Payload, ResourceRef}; +use n0_model::n0_xml; +use n0_model::n0_xml_source::{self, ErrorPhase, SourceProvider, SourceSnapshot, SourceVersion}; +use std::collections::BTreeMap; + +fn snapshot(identity: &str, base: &str, source: &str) -> SourceSnapshot { + SourceSnapshot::new(identity, base, source) +} + +#[derive(Default)] +struct MemoryProvider { + sources: BTreeMap<(String, String), SourceSnapshot>, + requests: Vec<(String, String)>, +} + +impl MemoryProvider { + fn insert(&mut self, from: &str, location: &str, source: SourceSnapshot) { + self.sources + .insert((from.to_owned(), location.to_owned()), source); + } +} + +impl SourceProvider for MemoryProvider { + fn resolve( + &mut self, + containing: &SourceSnapshot, + location: &str, + ) -> Result { + self.requests + .push((containing.identity().to_owned(), location.to_owned())); + self.sources + .get(&(containing.identity().to_owned(), location.to_owned())) + .cloned() + .ok_or_else(|| "not found".into()) + } +} + +#[test] +fn immutable_snapshot_and_exact_versioned_source_unit_are_public_outcomes() { + let source = r##" + + + +"##; + let unit = n0_xml_source::parse_source(snapshot("entry", "memory:/", source)).unwrap(); + + assert_eq!(unit.snapshot().identity(), "entry"); + assert_eq!(unit.snapshot().base(), "memory:/"); + assert_eq!(unit.snapshot().source(), source); + assert_eq!(unit.version(), SourceVersion::Draft1); + assert_eq!(unit.component_ids().collect::>(), ["badge"]); + assert!(unit.has_scene()); + + let version2 = n0_xml_source::parse_source(snapshot( + "future", + "memory:/", + r##""##, + )) + .unwrap(); + assert_eq!(version2.version(), SourceVersion::Version2); +} + +#[test] +fn draft1_source_grammar_is_strict_before_linking() { + let cases = [ + ( + r##""##, + "must precede the scene root", + ), + ( + r##""##, + "duplicate component", + ), + ( + r##""##, + "lowercase kebab-case", + ), + ( + r##""##, + "cannot declare x", + ), + ( + r##""##, + "requires an ID fragment", + ), + ( + r##""##, + "cannot contain character data or child elements", + ), + ( + " \n ", + "cannot contain character data", + ), + ( + r##""##, + "unknown attribute `width` on ", + ), + ( + r##""##, + "requires n0 XML Version 2", + ), + ]; + + for (source, expected) in cases { + let error = n0_xml_source::parse_source(snapshot("bad", "memory:/", source)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse, "{error}"); + assert!(error.to_string().contains(expected), "{error}"); + } +} + +#[test] +fn local_uses_materialize_as_independent_boxed_subtrees_in_caller_order() { + let source = r##" + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", source), &mut provider) + .unwrap(); + + assert!( + provider.requests.is_empty(), + "local uses require no provider call" + ); + let document_root = output.document.get(output.document.root); + let scene = document_root.children[0]; + let instances = &output.document.get(scene).children; + assert_eq!(instances.len(), 2); + assert_ne!(instances[0], instances[1]); + for &instance in instances { + assert!(matches!( + output.document.get(instance).payload, + Payload::Frame { .. } + )); + assert_eq!( + output.document.get(instance).header.name.as_deref(), + Some("Definition") + ); + assert_eq!(output.document.get(instance).children.len(), 1); + assert_ne!(output.document.get(instance).children[0], instances[0]); + assert_ne!(output.document.get(instance).children[0], instances[1]); + } + assert_eq!( + output.document.get(instances[0]).header.x, + AxisBinding::start(5.0) + ); + assert_eq!( + output.document.get(instances[1]).header.x, + AxisBinding::start(25.0) + ); + assert!(output.document.get(instances[0]).header.active); + assert!(!output.document.get(instances[1]).header.active); + + let first = output.provenance.get(&instances[0]).unwrap(); + let second = output.provenance.get(&instances[1]).unwrap(); + assert_eq!(first.component.as_ref().unwrap().source, "entry"); + assert_eq!(first.component.as_ref().unwrap().id, "badge"); + assert_eq!(first.use_chain.len(), 1); + assert_eq!(first.use_chain[0].name.as_deref(), Some("first")); + assert_eq!(second.use_chain[0].name.as_deref(), Some("second")); + assert_eq!( + output + .provenance + .get(&output.document.get(instances[0]).children[0]) + .unwrap() + .use_chain, + first.use_chain + ); +} + +#[test] +fn external_uses_load_one_retained_immutable_source_unit() { + let entry = r##" + + + + + + +"##; + let library = r##" + + + + + +"##; + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./library.n0.xml", + snapshot("library", "memory:/library/", library), + ); + + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + assert_eq!(provider.requests.len(), 1, "one lexical edge is cached"); + assert_eq!(output.program.entry(), "entry"); + assert_eq!(output.program.units().len(), 2); + assert_eq!( + output.program.unit("library").unwrap().snapshot().source(), + library + ); + let scene = output.document.get(output.document.root).children[0]; + assert_eq!(output.document.get(scene).children.len(), 2); +} + +#[test] +fn hidden_component_edges_still_participate_in_cycle_detection() { + let source = r##" + + + + + +"##; + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("cycle", "memory:/", source), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Link); + assert!( + error.message.contains("cycle#a -> cycle#b -> cycle#a"), + "{error}" + ); +} + +#[test] +fn equal_relative_image_strings_from_distinct_origins_get_distinct_runtime_rids() { + let entry = r##" + + + + + + +"##; + let component = |color: &str| { + format!( + r##""## + ) + }; + let a = component("#ff0000"); + let b = component("#0000ff"); + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./a.n0.xml", + snapshot("a", "memory:/a/", a.as_str()), + ); + provider.insert( + "entry", + "./b.n0.xml", + snapshot("b", "memory:/b/", b.as_str()), + ); + + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + assert_eq!(output.resources.len(), 2); + assert_eq!(output.resources[0].authored, "./texture.png"); + assert_eq!(output.resources[1].authored, "./texture.png"); + assert_eq!(output.resources[0].source, "a"); + assert_eq!(output.resources[1].source, "b"); + assert_eq!(output.resources[0].base, "memory:/a/"); + assert_eq!(output.resources[1].base, "memory:/b/"); + assert_ne!( + output.resources[0].runtime_rid, + output.resources[1].runtime_rid + ); + + let mut image_rids = vec![]; + for id in 0..output.document.capacity() as u32 { + let Some(node) = output.document.get_opt(id) else { + continue; + }; + for paint in node.fills.iter() { + if let Paint::Image(image) = paint { + let ResourceRef::Rid(rid) = &image.image else { + panic!("source materialization emits runtime RIDs") + }; + image_rids.push(rid.clone()); + } + } + } + image_rids.sort(); + let mut manifest_rids = output + .resources + .iter() + .map(|entry| entry.runtime_rid.clone()) + .collect::>(); + manifest_rids.sort(); + assert_eq!(image_rids, manifest_rids); +} + +#[test] +fn failures_keep_parse_resolution_and_link_phases_distinct() { + let entry = + r##""##; + let mut provider = MemoryProvider::default(); + let resolution = + n0_xml_source::materialize(snapshot("entry", "memory:/", entry), &mut provider) + .unwrap_err(); + assert_eq!(resolution.phase, ErrorPhase::Resolve); + assert!(resolution.message.contains("./missing.n0.xml")); + assert_eq!( + resolution.authored_use.as_ref().unwrap().href, + "./missing.n0.xml#x" + ); + + let unknown = r##""##; + let link = n0_xml_source::materialize(snapshot("entry", "memory:/", unknown), &mut provider) + .unwrap_err(); + assert_eq!(link.phase, ErrorPhase::Link); + assert!(link.message.contains("not defined")); + + let future = r##""##; + let direct = n0_xml_source::parse_source(snapshot("future", "memory:/", future)).unwrap_err(); + assert_eq!(direct.phase, ErrorPhase::Parse); + + let future_entry = + r##""##; + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./future.n0.xml", + snapshot("future", "memory:/future/", future), + ); + let dependency = + n0_xml_source::materialize(snapshot("entry", "memory:/", future_entry), &mut provider) + .unwrap_err(); + assert_eq!(dependency.phase, ErrorPhase::Resolve); + assert_eq!( + dependency.authored_use.as_ref().unwrap().href, + "./future.n0.xml#x" + ); + + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./invalid.n0.xml", + snapshot( + "", + "memory:/invalid/", + r##""##, + ), + ); + let invalid_snapshot = + r##""##; + let invalid = n0_xml_source::materialize( + snapshot("entry", "memory:/", invalid_snapshot), + &mut provider, + ) + .unwrap_err(); + assert_eq!(invalid.phase, ErrorPhase::Resolve); + assert!(invalid.message.contains("canonical source identity")); +} + +#[test] +fn canonical_identity_aliases_share_one_unit_and_inconsistent_snapshots_fail() { + let entry = r##" + + + + +"##; + let library = r##""##; + let mut provider = MemoryProvider::default(); + let canonical = snapshot("library", "memory:/library/", library); + provider.insert("entry", "./library.n0.xml", canonical.clone()); + provider.insert("entry", "./alias.n0.xml", canonical); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + assert_eq!(provider.requests.len(), 2); + assert_eq!(output.program.units().len(), 2); + let scene = output.document.get(output.document.root).children[0]; + assert_eq!(output.document.get(scene).children.len(), 2); + + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./library.n0.xml", + snapshot("library", "memory:/library/", library), + ); + provider.insert( + "entry", + "./alias.n0.xml", + snapshot( + "library", + "memory:/different/", + r##""##, + ), + ); + let error = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Resolve); + assert!(error.message.contains("inconsistent bytes or base")); + assert_eq!( + error.authored_use.as_ref().unwrap().href, + "./alias.n0.xml#card" + ); +} + +#[test] +fn image_manifest_covers_strokes_and_text_runs_and_dedupes_repeated_instances() { + let source = r##" + + + + + + run + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", source), &mut provider) + .unwrap(); + assert_eq!( + output.resources.len(), + 2, + "same origin+identifier dedupes across uses" + ); + assert_eq!( + output + .resources + .iter() + .map(|resource| resource.authored.as_str()) + .collect::>(), + ["./stroke.png", "./run.png"] + ); + + let mut stroke_images = 0; + let mut run_images = 0; + for id in 0..output.document.capacity() as u32 { + let Some(node) = output.document.get_opt(id) else { + continue; + }; + stroke_images += node + .strokes + .iter() + .flat_map(|stroke| stroke.paints.iter()) + .filter(|paint| matches!(paint, Paint::Image(_))) + .count(); + if let Payload::AttributedText { + attributed_string, .. + } = &node.payload + { + run_images += attributed_string + .runs + .iter() + .filter_map(|run| run.fills.as_ref()) + .flat_map(|fills| fills.iter()) + .filter(|paint| matches!(paint, Paint::Image(_))) + .count(); + } + } + assert_eq!(stroke_images, 2); + assert_eq!(run_images, 2); + assert_eq!(output.provenance.len() + 1, output.document.len()); + assert!(output + .provenance + .values() + .all(|provenance| { provenance.use_chain.is_empty() || provenance.use_chain.len() == 1 })); +} + +#[test] +fn use_relationship_context_is_validated_during_materialization() { + let source = r##" + + + + +"##; + n0_xml_source::parse_source(snapshot("entry", "memory:/", source)) + .expect("source-local syntax is valid before the target relationship is materialized"); + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Materialize); + assert!(error + .to_string() + .contains("flow is only valid on a child of a flex container")); + assert_eq!(error.source, "entry"); + assert_eq!(error.component.as_deref(), Some("item")); + assert_eq!(error.use_chain.len(), 1); + assert_eq!(error.use_chain[0].href, "#item"); + assert_eq!(error.use_chain[0].target.source, "entry"); + assert_eq!(error.use_chain[0].target.id, "item"); + + let valid = r##" + + + + + + +"##; + let output = n0_xml_source::materialize( + snapshot("valid", "memory:/", valid), + &mut MemoryProvider::default(), + ) + .expect("omitted flow is the canonical in-flow default"); + let scene = output.document.get(output.document.root).children[0]; + let instance = output.document.get(scene).children[0]; + assert_eq!(output.document.get(instance).header.grow, 1.0); +} + +#[test] +fn draft0_source_materialization_rekeys_images_without_changing_the_draft0_api() { + let source = r##""##; + let ordinary = n0_xml::parse(source).unwrap(); + let original_rect = ordinary + .get(ordinary.get(ordinary.root).children[0]) + .children[0]; + let Paint::Image(original) = &ordinary.get(original_rect).fills[0] else { + panic!("image") + }; + assert_eq!(original.image, ResourceRef::Rid("./image.png".into())); + + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", source), &mut provider) + .unwrap(); + let scene = output.document.get(output.document.root).children[0]; + let rect = output.document.get(scene).children[0]; + let Paint::Image(materialized) = &output.document.get(rect).fills[0] else { + panic!("image") + }; + assert_eq!(output.resources.len(), 1); + assert_eq!(output.resources[0].authored, "./image.png"); + assert_eq!( + materialized.image, + ResourceRef::Rid(output.resources[0].runtime_rid.clone()) + ); + assert_eq!(n0_xml::VERSION, "0"); +} + +#[test] +fn draft0_parse_and_print_contract_remains_unchanged() { + let source = r##""##; + let ordinary = n0_xml::parse(source).unwrap(); + let mut provider = MemoryProvider::default(); + let linked = + n0_xml_source::materialize(snapshot("draft0", "memory:/", source), &mut provider).unwrap(); + assert_eq!(ordinary, linked.document); + assert_eq!(linked.program.units().len(), 1); + assert_eq!( + linked.program.unit("draft0").unwrap().version(), + SourceVersion::Draft0 + ); + assert!(n0_xml::print(&ordinary) + .unwrap() + .starts_with("")); +} diff --git a/crates/n0-model/tests/n0_xml_source_v2.rs b/crates/n0-model/tests/n0_xml_source_v2.rs new file mode 100644 index 00000000..770b956b --- /dev/null +++ b/crates/n0-model/tests/n0_xml_source_v2.rs @@ -0,0 +1,691 @@ +//! Version 2 typed scalar specialization contract. + +use n0_model::model::{BoxFit, ImagePaintFit, Paint, Payload, ResourceRef, SizeIntent}; +use n0_model::n0_xml_source::{ + self, BindingTargetKind, ErrorPhase, ScalarType, SourceProvider, SourceSnapshot, SourceVersion, + ValueSelection, +}; +use std::collections::BTreeMap; + +fn snapshot(identity: &str, base: &str, source: &str) -> SourceSnapshot { + SourceSnapshot::new(identity, base, source) +} + +#[derive(Default)] +struct MemoryProvider(BTreeMap<(String, String), SourceSnapshot>); + +impl MemoryProvider { + fn insert(&mut self, from: &str, location: &str, source: SourceSnapshot) { + self.0 + .insert((from.to_owned(), location.to_owned()), source); + } +} + +impl SourceProvider for MemoryProvider { + fn resolve( + &mut self, + containing: &SourceSnapshot, + location: &str, + ) -> Result { + self.0 + .get(&(containing.identity().to_owned(), location.to_owned())) + .cloned() + .ok_or_else(|| "not found".into()) + } +} + +fn authored_scene(output: &n0_xml_source::MaterializedProgram) -> u32 { + output.document.get(output.document.root).children[0] +} + +#[test] +fn all_six_scalar_types_specialize_into_the_existing_ordinary_model() { + let source = r##" + + + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", source), &mut provider) + .unwrap(); + + assert_eq!( + output.program.unit("entry").unwrap().version(), + SourceVersion::Version2 + ); + let scene = authored_scene(&output); + let card = output.document.get(scene).children[0]; + let card_node = output.document.get(card); + assert_eq!(card_node.header.name.as_deref(), Some("Feature")); + assert_eq!(card_node.header.width, SizeIntent::Fixed(120.0)); + assert!(!card_node.header.active); + let Paint::Solid(background) = &card_node.fills[0] else { + panic!("color prop materializes through ordinary compact fill") + }; + assert_eq!(background.color.0, 0xFF12_3456); + + let rect = card_node.children[0]; + let Paint::Image(image) = &output.document.get(rect).fills[0] else { + panic!("resource prop materializes as image paint") + }; + assert_eq!(image.fit, ImagePaintFit::Fit(BoxFit::Contain)); + let ResourceRef::Rid(runtime_rid) = &image.image else { + panic!("source materialization uses runtime RID") + }; + assert_eq!(runtime_rid, &output.resources[0].runtime_rid); + assert_eq!(output.resources[0].source, "entry"); + assert_eq!(output.resources[0].authored, "./noise.png"); + + let text = card_node.children[1]; + let Payload::Text { content, .. } = &output.document.get(text).payload else { + panic!("string prop materializes into ordinary text") + }; + assert_eq!(content, "Feature"); + + let specialization = &output.specializations[0]; + assert_eq!(specialization.component.id, "card"); + assert_eq!( + specialization + .props + .iter() + .map(|prop| prop.scalar_type) + .collect::>(), + [ + ScalarType::String, + ScalarType::Boolean, + ScalarType::Number, + ScalarType::Color, + ScalarType::Enum, + ScalarType::Resource, + ] + ); + assert!(specialization + .props + .iter() + .all(|prop| matches!(prop.selection, ValueSelection::Supplied { .. }))); + let title = specialization + .props + .iter() + .find(|prop| prop.name == "title") + .unwrap(); + assert_eq!(title.binding_targets.len(), 2); + assert_eq!(title.materialized_occurrences.len(), 2); + assert!(title.binding_targets.iter().any( + |target| matches!(&target.kind, BindingTargetKind::Attribute { name } if name == "name") + )); + assert!(title + .binding_targets + .iter() + .any(|target| matches!(target.kind, BindingTargetKind::Text { .. }))); + let texture = specialization + .props + .iter() + .find(|prop| prop.name == "texture") + .unwrap(); + assert_eq!(texture.binding_targets.len(), 1); + assert_eq!(texture.materialized_occurrences.len(), 1); + assert_eq!(texture.materialized_occurrences[0].node, rect); +} + +#[test] +fn defaults_and_explicit_values_that_equal_defaults_keep_distinct_selection() { + let source = r##" + + + + + {label} + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider).unwrap(); + assert_eq!(output.specializations.len(), 2); + let first = output.specializations[0] + .props + .iter() + .find(|prop| prop.name == "accent") + .unwrap(); + let second = output.specializations[1] + .props + .iter() + .find(|prop| prop.name == "accent") + .unwrap(); + assert_eq!(first.value, second.value); + assert_eq!(first.selection, ValueSelection::CalleeDefault); + assert!(matches!(second.selection, ValueSelection::Supplied { .. })); + assert_eq!(first.ultimate_origin.source, "entry"); + assert_eq!(second.ultimate_origin.source, "entry"); +} + +#[test] +fn empty_string_presence_distinguishes_defaults_from_supplied_arguments() { + let source = r##" + + + + {value} + + + + + + +"##; + let output = n0_xml_source::materialize( + snapshot("entry", "memory:/", source), + &mut MemoryProvider::default(), + ) + .expect("empty strings are present values, not null or omission"); + assert_eq!(output.specializations.len(), 2); + let defaulted = &output.specializations[0].props[0]; + let supplied = &output.specializations[1].props[0]; + assert_eq!(defaulted.value, ""); + assert_eq!(supplied.value, ""); + assert_eq!(defaulted.selection, ValueSelection::CalleeDefault); + assert!(matches!( + supplied.selection, + ValueSelection::Supplied { .. } + )); +} + +#[test] +fn nested_forwarding_preserves_the_ultimate_resource_literal_origin() { + let entry = r##" + + + + + + + + +"##; + let library = r##" + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./library.n0.xml", + snapshot("library", "memory:/library/", library), + ); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + + assert_eq!(output.resources.len(), 2); + assert_eq!(output.resources[0].source, "entry"); + assert_eq!(output.resources[0].authored, "./caller.png"); + assert_eq!(output.resources[1].source, "library"); + assert_eq!(output.resources[1].authored, "./default.png"); + + let leafs = output + .specializations + .iter() + .filter(|specialization| specialization.component.id == "leaf") + .collect::>(); + assert_eq!(leafs.len(), 2); + assert_eq!(leafs[0].props[0].ultimate_origin.source, "entry"); + assert_eq!(leafs[1].props[0].ultimate_origin.source, "library"); + for leaf in leafs { + assert!(matches!( + leaf.props[0].selection, + ValueSelection::Supplied { .. } + )); + assert_eq!(leaf.props[0].forwarding.len(), 1); + assert_eq!(leaf.props[0].forwarding[0].source, "library"); + assert_eq!(leaf.props[0].forwarding[0].name, "texture"); + } + + let wrappers = output + .specializations + .iter() + .filter(|specialization| specialization.component.id == "wrapper") + .collect::>(); + assert_eq!(wrappers.len(), 2); + for wrapper in wrappers { + let asset = &wrapper.props[0]; + assert_eq!(asset.name, "asset"); + assert_eq!(asset.binding_targets.len(), 1); + assert!(asset.materialized_occurrences.is_empty()); + let target = &asset.binding_targets[0]; + assert_eq!(target.source, "library"); + assert_eq!(target.element, "arg"); + assert!(target.span.end > target.span.start); + assert!(matches!( + &target.kind, + BindingTargetKind::Argument { use_href, argument } + if use_href == "#leaf" && argument == "texture" + )); + } +} + +#[test] +fn brace_scanning_is_source_local_and_substituted_strings_are_single_pass() { + let source = r##" + + + + literal {{label}}; value {label} + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider).unwrap(); + let scene = authored_scene(&output); + let message = output.document.get(scene).children[0]; + assert_eq!( + output.document.get(message).header.name.as_deref(), + Some("{literal}") + ); + let text = output.document.get(message).children[0]; + let Payload::Text { content, .. } = &output.document.get(text).payload else { + panic!("text") + }; + assert_eq!(content, "literal {label}; value {other}"); +} + +#[test] +fn version2_accepts_version1_static_components_without_reinterpreting_braces() { + let entry = + r##""##; + let v1 = r##"{price} {{price}}"##; + let mut provider = MemoryProvider::default(); + provider.insert("entry", "./v1.n0.xml", snapshot("v1", "memory:/v1/", v1)); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + let scene = authored_scene(&output); + let instance = output.document.get(scene).children[0]; + let text = output.document.get(instance).children[0]; + let Payload::Text { content, .. } = &output.document.get(text).payload else { + panic!("text") + }; + assert_eq!(content, "{price} {{price}}"); + assert!(output.specializations.is_empty()); + + let v1_entry = + r##""##; + let v2 = r##""##; + let mut provider = MemoryProvider::default(); + provider.insert("v1-entry", "./v2.n0.xml", snapshot("v2", "memory:/v2/", v2)); + let error = n0_xml_source::materialize( + snapshot("v1-entry", "memory:/entry/", v1_entry), + &mut provider, + ) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Link); + assert!(error.authored_use.is_some()); +} + +#[test] +fn declaration_and_binding_failures_are_source_errors() { + let cases = [ + ( + r##""##, + "reserved", + ), + ( + r##""##, + "duplicate prop", + ), + ( + r##""##, + "has no binding or forwarding site", + ), + ( + r##"{n}"##, + "cannot bind to text content", + ), + ( + r##"{missing}"##, + "is not declared", + ), + ( + r##""##, + "interpolation is invalid", + ), + ( + r##"{label"##, + "unescaped `{`", + ), + ( + r##""##, + "must precede fill, stroke, and render children", + ), + ( + r##""##, + "not a valid color", + ), + ( + r##""##, + "member `stretch` is not accepted", + ), + ( + r##""##, + "opacity must be between 0 and 1", + ), + ]; + for (source, expected) in cases { + let error = n0_xml_source::parse_source(snapshot("bad", "memory:/", source)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse, "{error}"); + assert!(error.to_string().contains(expected), "{error}"); + } +} + +#[test] +fn specialization_rejects_missing_unknown_and_mistyped_arguments() { + let cases = [ + ( + r##""##, + "is required", + ), + ( + r##""##, + "unknown argument", + ), + ( + r##""##, + "must be exactly", + ), + ( + r##""##, + "must be finite", + ), + ( + r##""##, + "not a valid color", + ), + ( + r##""##, + "not a member", + ), + ( + r##""##, + "non-empty", + ), + ]; + for (source, expected) in cases { + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("bad", "memory:/", source), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Specialize, "{error}"); + assert!(error.to_string().contains(expected), "{error}"); + } +} + +#[test] +fn forwarding_requires_exact_types_and_enum_subset_compatibility() { + let cases = [ + ( + r##" + + + + +"##, + "cannot forward", + ), + ( + r##" + + + + +"##, + "source member `fill` is not accepted", + ), + ]; + for (source, expected) in cases { + let mut provider = MemoryProvider::default(); + let error = + n0_xml_source::materialize(snapshot("bad-forward", "memory:/", source), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Specialize, "{error}"); + assert!(error.to_string().contains(expected), "{error}"); + } +} + +#[test] +fn specialized_values_run_the_existing_target_specific_validation() { + let source = r##" + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize( + snapshot("target-invalid", "memory:/", source), + &mut provider, + ) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Specialize); + assert!(error + .to_string() + .contains("opacity must be between 0 and 1")); + assert_eq!(error.use_chain.len(), 1); + assert_eq!(error.component.as_deref(), Some("surface")); + assert_eq!(error.specialization_sites.len(), 1); + let site = &error.specialization_sites[0]; + assert_eq!(site.prop, "alpha"); + assert!(matches!( + &site.selection, + ValueSelection::Supplied { argument } if argument.source == "target-invalid" + )); + assert!(matches!( + &site.binding.kind, + BindingTargetKind::Attribute { name } if name == "opacity" + )); + assert_eq!(site.binding.source, "target-invalid"); + assert!(error.to_string().contains("prop `alpha` from argument")); + assert!(error.to_string().contains("bound at target-invalid")); +} + +#[test] +fn version2_use_cannot_send_arguments_to_a_version1_component() { + let entry = r##" + +"##; + let v1 = r##""##; + let mut provider = MemoryProvider::default(); + provider.insert("entry", "./v1.n0.xml", snapshot("v1", "memory:/v1/", v1)); + let error = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Specialize); + assert!(error.to_string().contains("empty interface")); +} + +#[test] +fn duplicate_args_and_top_level_forwarding_are_rejected_during_source_parse() { + let cases = [ + ( + r##""##, + "duplicate argument", + ), + ( + r##""##, + "cannot forward without an enclosing component scope", + ), + ]; + for (source, expected) in cases { + let error = n0_xml_source::parse_source(snapshot("bad", "memory:/", source)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse); + assert!(error.to_string().contains(expected), "{error}"); + } +} + +#[test] +fn required_props_do_not_acquire_invented_values_during_source_validation() { + let free_layout = r##" + + + + + + + +"##; + n0_xml_source::parse_source(snapshot("free", "memory:/", free_layout)) + .expect("source validation must select a witness from the declared enum domain"); + n0_xml_source::materialize( + snapshot("free", "memory:/", free_layout), + &mut MemoryProvider::default(), + ) + .expect("the caller-supplied free-layout branch is valid"); + + for (layout_values, flow_values) in [("none flex", "in absolute"), ("flex none", "absolute in")] + { + let source = format!( + r##" + + + + + + + + + + + + + +"## + ); + n0_xml_source::parse_source(snapshot("positioned", "memory:/", &source)) + .expect("source validation searches declared enum combinations independent of order"); + n0_xml_source::materialize( + snapshot("positioned", "memory:/", &source), + &mut MemoryProvider::default(), + ) + .expect("the supplied flex/absolute combination is valid"); + } + + let auto_axis = r##" + + + + + + + +"##; + n0_xml_source::parse_source(snapshot("auto", "memory:/", auto_axis)) + .expect("enum witnesses must preserve the declared auto sizing branch"); + n0_xml_source::materialize( + snapshot("auto", "memory:/", auto_axis), + &mut MemoryProvider::default(), + ) + .expect("auto plus aspect ratio supplies exactly one primitive axis"); + + let bounded_stop = r##" + + + + + + + + + + + + + + + +"##; + n0_xml_source::parse_source(snapshot("gradient", "memory:/", bounded_stop)) + .expect("numeric source validation derives witnesses from authored bounds"); + n0_xml_source::materialize( + snapshot("gradient", "memory:/", bounded_stop), + &mut MemoryProvider::default(), + ) + .expect("the supplied stop remains between its authored neighbors"); + + for values in ["none flex", "flex none"] { + let source = format!( + r##" + + + + +"## + ); + n0_xml_source::parse_source(snapshot("entry", "memory:/", &source)) + .expect("required enum member order must not create a source-time effective value"); + let mut provider = MemoryProvider::default(); + n0_xml_source::materialize(snapshot("entry", "memory:/", &source), &mut provider) + .expect("the caller-supplied flex value satisfies the complete ordinary state"); + } + + let source = r##" + + + + +"##; + n0_xml_source::parse_source(snapshot("entry", "memory:/", source)) + .expect("required number props remain deferred during source validation"); + let mut provider = MemoryProvider::default(); + n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider) + .expect("zero smoothing is compatible with elliptical corners"); + + let incompatible = source.replace("value=\"0\"", "value=\"1\""); + let mut provider = MemoryProvider::default(); + let error = + n0_xml_source::materialize(snapshot("entry", "memory:/", &incompatible), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Specialize); + assert!(error.message.contains("requires circular corner radii")); + assert_eq!(error.specialization_sites.len(), 1); + assert_eq!(error.specialization_sites[0].prop, "smoothing"); +} diff --git a/crates/n0-model/tests/n0_xml_source_v3.rs b/crates/n0-model/tests/n0_xml_source_v3.rs new file mode 100644 index 00000000..aea2c43a --- /dev/null +++ b/crates/n0-model/tests/n0_xml_source_v3.rs @@ -0,0 +1,804 @@ +//! Version 3 named static slot projection producer contract. + +use n0_model::model::{AxisBinding, Document, NodeId, Paint}; +use n0_model::n0_xml; +use n0_model::n0_xml_source::{ + self, BindingTargetKind, ErrorPhase, SourceProvider, SourceSnapshot, SourceVersion, +}; +use std::collections::BTreeMap; + +fn snapshot(identity: &str, base: &str, source: &str) -> SourceSnapshot { + SourceSnapshot::new(identity, base, source) +} + +#[derive(Default)] +struct MemoryProvider { + sources: BTreeMap<(String, String), SourceSnapshot>, + requests: Vec<(String, String)>, +} + +impl MemoryProvider { + fn insert(&mut self, from: &str, location: &str, source: SourceSnapshot) { + self.sources + .insert((from.to_owned(), location.to_owned()), source); + } +} + +impl SourceProvider for MemoryProvider { + fn resolve( + &mut self, + containing: &SourceSnapshot, + location: &str, + ) -> Result { + self.requests + .push((containing.identity().to_owned(), location.to_owned())); + self.sources + .get(&(containing.identity().to_owned(), location.to_owned())) + .cloned() + .ok_or_else(|| "not found".into()) + } +} + +fn authored_scene(output: &n0_xml_source::MaterializedProgram) -> NodeId { + output.document.get(output.document.root).children[0] +} + +fn solid_color(document: &Document, node: NodeId) -> u32 { + let Paint::Solid(solid) = &document.get(node).fills[0] else { + panic!("expected a solid fill") + }; + solid.color.0 +} + +#[test] +fn version3_alone_accepts_unique_named_empty_slots_in_component_render_positions() { + let source = r##" + + + + + + +"##; + let unit = n0_xml_source::parse_source(snapshot("entry", "memory:/", source)).unwrap(); + assert_eq!(unit.version(), SourceVersion::Version3); + assert_eq!(unit.component_ids().collect::>(), ["card"]); + + for version in ["0", "1", "2"] { + let source = format!( + r##""## + ); + let error = n0_xml_source::parse_source(snapshot("old", "memory:/", &source)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse, "{error}"); + } + + let outside = r##""##; + let error = n0_xml_source::parse_source(snapshot("outside", "memory:/", outside)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse); + assert!(error.message.contains("only in a Version 3 component")); + + let leaf = r##""##; + let error = n0_xml_source::parse_source(snapshot("leaf", "memory:/", leaf)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse); + assert!(error.message.contains("not valid inside ")); +} + +#[test] +fn malformed_duplicate_and_misplaced_slot_syntax_has_focused_parse_diagnostics() { + let cases = [ + ( + r##""##, + "requires exactly `name`", + ), + ( + r##""##, + "lowercase kebab-case", + ), + ( + r##""##, + "requires exactly `name`", + ), + ( + r##"text"##, + "must be empty", + ), + ( + r##""##, + "duplicate slot `body`", + ), + ( + r##""##, + "valid only on a direct Version 3 ", + ), + ( + r##""##, + "requires `slot`", + ), + ( + r##""##, + "must be direct render roots", + ), + ( + r##"{label}"##, + "must precede every render assignment", + ), + ]; + + for (source, expected) in cases { + let error = n0_xml_source::parse_source(snapshot("bad", "memory:/", source)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse, "{error}"); + assert!(error.message.contains(expected), "{error}"); + } +} + +#[test] +fn slot_markers_start_render_content_for_property_ordering_regardless_of_projection() { + let late_properties = [ + r##""##, + r##""##, + ]; + let assignments = ["", r##""##]; + + for late_property in late_properties { + for assignment in assignments { + let source = format!( + r##" + + + + {late_property} + + {assignment} + +"## + ); + let error = + n0_xml_source::parse_source(snapshot("late", "memory:/", &source)).unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Parse, "{error}"); + assert!( + error + .message + .contains("must appear before content or scene children"), + "{error}" + ); + } + } +} + +#[test] +fn leading_paint_properties_remain_valid_before_slots_for_empty_and_populated_uses() { + let source = r##" + + + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("ordered", "memory:/", source), &mut provider).unwrap(); + let instances = &output.document.get(authored_scene(&output)).children; + assert_eq!(instances.len(), 2); + assert!(output.document.get(instances[0]).children.is_empty()); + assert_eq!(output.document.get(instances[1]).children.len(), 1); +} + +#[test] +fn zero_one_and_many_assignments_project_at_the_marker_in_caller_order() { + let source = r##" + + + + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider).unwrap(); + let scene = authored_scene(&output); + let cards = &output.document.get(scene).children; + assert_eq!(cards.len(), 2); + for &card in cards { + assert_eq!(output.document.get(card).children.len(), 3); + } + let empty_frame = output.document.get(cards[0]).children[1]; + let filled_frame = output.document.get(cards[1]).children[1]; + assert!(output.document.get(empty_frame).children.is_empty()); + let assigned = &output.document.get(filled_frame).children; + assert_eq!(assigned.len(), 2); + assert_eq!( + output.document.get(assigned[0]).header.name.as_deref(), + Some("first") + ); + assert_eq!( + output.document.get(assigned[1]).header.name.as_deref(), + Some("second") + ); + + let printed = n0_xml::print(&output.document).unwrap(); + assert!(!printed.contains(">() + .as_slice(), + assigned.as_slice() + ); + assert!(filled + .assignments + .iter() + .all(|assignment| assignment.site.source == "entry" + && assignment.site.component.is_none() + && assignment.site.name == "media")); + assert_eq!(output.provenance[&assigned[0]].source, "entry"); + assert_eq!(output.provenance[&assigned[0]].use_chain.len(), 1); +} + +#[test] +fn assignment_root_span_bindings_keep_their_size_evidence() { + let source = r##" + + + + + + + + + +"##; + let output = n0_xml_source::materialize( + snapshot("entry", "memory:/", source), + &mut MemoryProvider::default(), + ) + .expect("span bindings supply projected primitive extents"); + let scene = authored_scene(&output); + let instance = output.document.get(scene).children[0]; + let [rect, line] = output.document.get(instance).children.as_slice() else { + panic!("expected projected rect and line") + }; + assert_eq!( + output.document.get(*rect).header.x, + AxisBinding::Span { + start: 0.0, + end: 0.0 + } + ); + assert_eq!( + output.document.get(*rect).header.y, + AxisBinding::Span { + start: 0.0, + end: 0.0 + } + ); + assert!(matches!( + output.document.get(*line).header.x, + AxisBinding::Span { .. } + )); +} + +#[test] +fn unknown_assignments_are_link_errors_and_only_version3_targets_receive_them() { + let unknown = r##" + + + + +"##; + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("entry", "memory:/", unknown), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Link); + assert!(error.message.contains("unknown slot assignment `missing`")); + assert!(error.message.contains("available: body")); + + let entry = r##""##; + let v2 = r##""##; + let mut provider = MemoryProvider::default(); + provider.insert("entry", "./v2.n0.xml", snapshot("v2", "memory:/v2/", v2)); + let error = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Link); + assert!(error.message.contains("require a Version 3 target")); +} + +#[test] +fn assigned_roots_observe_the_linked_slot_parents_relationship_rules() { + let source = r##" + + + + + + +"##; + n0_xml_source::parse_source(snapshot("entry", "memory:/", source)) + .expect("the assignment root has no parent relationship until linking"); + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Projection); + assert!(error + .message + .contains("x/y are not valid on in-flow ")); + assert_eq!(error.source, "entry"); + assert_eq!( + error.component, None, + "the assigned root is caller-authored" + ); + assert_eq!(error.use_chain.len(), 1); + assert_eq!(error.use_chain[0].target.id, "row"); + let projection = error + .slot_projection + .as_ref() + .expect("projected relationship error retains both authored sites"); + assert_eq!(projection.definition.source, "entry"); + assert_eq!(projection.definition.component.id, "row"); + assert_eq!(projection.definition.name, "item"); + assert!(projection.definition.span.end > projection.definition.span.start); + assert_eq!(projection.receiving_use.target.id, "row"); + assert_eq!(projection.receiving_use, error.use_chain[0]); + assert_eq!(projection.assignment.source, "entry"); + assert_eq!(projection.assignment.component, None); + assert_eq!(projection.assignment.name, "item"); + assert!(projection.assignment.span.end > projection.assignment.span.start); +} + +#[test] +fn caller_scalar_errors_inside_slots_keep_caller_and_projection_ownership() { + let source = r##" + + + + + + + + + + + + + +"##; + let error = n0_xml_source::materialize( + snapshot("entry", "memory:/", source), + &mut MemoryProvider::default(), + ) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Specialize); + assert_eq!(error.source, "entry"); + assert_eq!(error.component.as_deref(), Some("outer")); + assert!(error.message.contains("opacity must be between 0 and 1")); + assert_eq!(error.specialization_sites.len(), 2); + let specialization = error + .specialization_sites + .iter() + .find(|site| site.prop == "alpha") + .expect("the failing opacity binding remains among the candidates"); + assert_eq!(specialization.prop, "alpha"); + assert_eq!(specialization.ultimate_origin.authored, "2"); + assert_eq!(specialization.binding.element, "rect"); + assert_eq!( + specialization.binding.kind, + BindingTargetKind::Attribute { + name: "opacity".into() + } + ); + assert!(error.specialization_sites.iter().any(|site| { + site.prop == "smooth" + && site.binding.kind + == BindingTargetKind::Attribute { + name: "corner-smoothing".into(), + } + })); + let projection = error + .slot_projection + .as_ref() + .expect("caller-owned scalar failure retains the receiving slot edge"); + assert_eq!(projection.definition.component.id, "shell"); + assert_eq!(projection.definition.name, "body"); + assert_eq!( + projection.receiving_use.name.as_deref(), + Some("inner-shell") + ); + assert_eq!( + projection.assignment.component.as_ref().unwrap().id, + "outer" + ); +} + +#[test] +fn bound_flow_projection_errors_keep_the_causal_scalar_site() { + let source = r##" + + + + + + + + + + + + +"##; + let error = n0_xml_source::materialize( + snapshot("entry", "memory:/", source), + &mut MemoryProvider::default(), + ) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Projection); + assert!(error.message.contains("x/y are not valid on in-flow")); + assert_eq!(error.specialization_sites.len(), 1); + let site = &error.specialization_sites[0]; + assert_eq!(site.prop, "mode"); + assert_eq!( + site.binding.kind, + BindingTargetKind::Attribute { + name: "flow".into() + } + ); + let projection = error.slot_projection.as_ref().unwrap(); + assert_eq!(projection.definition.component.id, "row"); + assert_eq!(projection.definition.name, "item"); + assert_eq!( + projection.assignment.component.as_ref().unwrap().id, + "outer" + ); +} + +#[test] +fn caller_authored_resources_keep_the_caller_base_across_files() { + let entry = r##" + + + +"##; + let library = r##" + + + + +"##; + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./library.n0.xml", + snapshot("library", "memory:/library/", library), + ); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + assert_eq!(output.resources.len(), 2); + let definition_resource = output + .resources + .iter() + .find(|resource| resource.source == "library") + .unwrap(); + let assignment_resource = output + .resources + .iter() + .find(|resource| resource.source == "entry") + .unwrap(); + assert_eq!(definition_resource.base, "memory:/library/"); + assert_eq!(assignment_resource.base, "memory:/entry/"); + assert_eq!(definition_resource.authored, "./texture.png"); + assert_eq!(assignment_resource.authored, "./texture.png"); + assert_ne!( + definition_resource.runtime_rid, + assignment_resource.runtime_rid + ); +} + +#[test] +fn assigned_content_uses_the_caller_prop_environment_not_the_callees_same_name() { + let source = r##" + + + + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider).unwrap(); + let wrapper = output.document.get(authored_scene(&output)).children[0]; + let shell = output.document.get(wrapper).children[0]; + let assigned = output.document.get(shell).children[0]; + assert_eq!(solid_color(&output.document, shell), 0xFF11_1111); + assert_eq!(solid_color(&output.document, assigned), 0xFF22_CC44); + + let wrapper_specialization = output + .specializations + .iter() + .find(|specialization| specialization.component.id == "wrapper") + .unwrap(); + let tone = wrapper_specialization + .props + .iter() + .find(|prop| prop.name == "tone") + .unwrap(); + assert_eq!(tone.materialized_occurrences.len(), 1); + assert_eq!(tone.materialized_occurrences[0].node, assigned); + assert_eq!( + output.provenance[&assigned].component.as_ref().unwrap().id, + "wrapper" + ); +} + +#[test] +fn nested_uses_in_assignments_resolve_from_the_callers_source() { + let entry = r##" + + + +"##; + let shell = + r##""##; + let parts = r##""##; + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./shell.n0.xml", + snapshot("shell", "memory:/shell/", shell), + ); + provider.insert( + "entry", + "./parts.n0.xml", + snapshot("parts", "memory:/parts/", parts), + ); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + assert_eq!( + provider.requests, + [ + ("entry".into(), "./shell.n0.xml".into()), + ("entry".into(), "./parts.n0.xml".into()), + ] + ); + let shell_node = output.document.get(authored_scene(&output)).children[0]; + let badge = output.document.get(shell_node).children[0]; + assert_eq!(output.provenance[&badge].source, "parts"); + assert_eq!(output.provenance[&badge].use_chain.len(), 2); + assert_eq!(output.slot_projections[0].assignments[0].node, badge); +} + +#[test] +fn slots_nested_in_caller_assignment_trees_keep_the_callers_slot_instance() { + let source = r##" + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/", source), &mut provider).unwrap(); + let outer = output.document.get(authored_scene(&output)).children[0]; + let inner = output.document.get(outer).children[0]; + let assignment_container = output.document.get(inner).children[0]; + let leaf = output.document.get(assignment_container).children[0]; + assert_eq!( + output.document.get(leaf).header.name.as_deref(), + Some("leaf") + ); + + assert_eq!(output.slot_projections.len(), 2); + let inner_projection = output + .slot_projections + .iter() + .find(|projection| projection.definition.name == "inner-body") + .unwrap(); + let outer_projection = output + .slot_projections + .iter() + .find(|projection| projection.definition.name == "outer-body") + .unwrap(); + assert_eq!(inner_projection.assignments[0].node, assignment_container); + assert_eq!(outer_projection.assignments[0].node, leaf); + assert_eq!(outer_projection.definition.component.id, "outer"); + assert_eq!(outer_projection.use_chain.len(), 1); + assert_eq!(outer_projection.use_chain[0].target.id, "outer"); +} + +#[test] +fn nested_projection_errors_identify_the_exact_receiving_use() { + let source = r##" + + + + + + + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("nested", "memory:/", source), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Projection); + assert!(error.message.contains("grow/align on "), "{error}"); + assert_eq!(error.use_chain.len(), 2); + assert_eq!(error.use_chain[0].name.as_deref(), Some("outer-instance")); + assert_eq!(error.use_chain[1].name.as_deref(), Some("inner-instance")); + + let projection = error + .slot_projection + .as_ref() + .expect("nested projected failure retains its precise projection edge"); + assert_eq!(projection.definition.component.id, "outer"); + assert_eq!(projection.definition.name, "outer-body"); + assert_eq!(projection.receiving_use.target.id, "outer"); + assert_eq!( + projection.receiving_use.name.as_deref(), + Some("outer-instance") + ); + assert_eq!(projection.receiving_use, error.use_chain[0]); + assert_ne!(projection.receiving_use, *error.use_chain.last().unwrap()); + assert_eq!(projection.assignment.component, None); +} + +#[test] +fn projected_content_drops_only_the_current_callee_from_cycle_detection() { + let finite = r##" + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = n0_xml_source::materialize(snapshot("finite", "memory:/", finite), &mut provider) + .expect("a caller-owned nested use of the current callee is a finite second instance"); + let outer = output.document.get(authored_scene(&output)).children[0]; + let inner = output.document.get(outer).children[0]; + assert!(output.document.get(inner).children.is_empty()); + assert_eq!(output.slot_projections.len(), 2); + assert_eq!(output.slot_projections[0].assignments[0].node, inner); + assert!(output.slot_projections[1].assignments.is_empty()); + + let cycle = r##" + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let error = n0_xml_source::materialize(snapshot("cycle", "memory:/", cycle), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Link); + assert!( + error.message.contains("cycle#outer -> cycle#outer"), + "{error}" + ); + assert_eq!(error.use_chain.len(), 3); + assert_eq!(error.use_chain[0].target.id, "outer"); + assert_eq!(error.use_chain[1].target.id, "card"); + assert_eq!(error.use_chain[2].target.id, "outer"); + let projection = error + .slot_projection + .as_ref() + .expect("projected cycle retains the edge that closes it"); + assert_eq!(projection.definition.source, "cycle"); + assert_eq!(projection.definition.component.id, "card"); + assert_eq!(projection.definition.name, "body"); + assert!(projection.definition.span.end > projection.definition.span.start); + assert_eq!(projection.receiving_use.target.id, "card"); + assert_eq!(projection.receiving_use, error.use_chain[1]); + assert_eq!(projection.assignment.source, "cycle"); + assert_eq!( + projection.assignment.component.as_ref().unwrap().id, + "outer" + ); + assert_eq!(projection.assignment.name, "body"); + assert!(projection.assignment.span.end > projection.assignment.span.start); +} + +#[test] +fn version3_links_versions_one_two_and_three_without_backporting_version3() { + let entry = r##" + + + + + +"##; + let v1 = r##""##; + let v2 = r##""##; + let v3 = + r##""##; + let mut provider = MemoryProvider::default(); + provider.insert("entry", "./v1.n0.xml", snapshot("v1", "memory:/v1/", v1)); + provider.insert("entry", "./v2.n0.xml", snapshot("v2", "memory:/v2/", v2)); + provider.insert("entry", "./v3.n0.xml", snapshot("v3", "memory:/v3/", v3)); + let output = + n0_xml_source::materialize(snapshot("entry", "memory:/entry/", entry), &mut provider) + .unwrap(); + assert_eq!( + output.document.get(authored_scene(&output)).children.len(), + 3 + ); + assert_eq!(output.slot_projections.len(), 1); + assert_eq!(output.specializations.len(), 2); + + for old_version in ["1", "2"] { + let old_entry = format!( + r##""## + ); + let mut provider = MemoryProvider::default(); + provider.insert("old", "./v3.n0.xml", snapshot("v3", "memory:/v3/", v3)); + let error = + n0_xml_source::materialize(snapshot("old", "memory:/old/", &old_entry), &mut provider) + .unwrap_err(); + assert_eq!(error.phase, ErrorPhase::Link); + assert!(error.message.contains(&format!( + "Version {old_version} source cannot link Version 3" + ))); + } +} diff --git a/crates/n0-model/tests/n0_xml_source_v4.rs b/crates/n0-model/tests/n0_xml_source_v4.rs new file mode 100644 index 00000000..a676157f --- /dev/null +++ b/crates/n0-model/tests/n0_xml_source_v4.rs @@ -0,0 +1,382 @@ +//! Version 4 durable authored-member and component-occurrence addresses. + +use n0_model::n0_xml_source::{ + self, AddressLookupError, AuthoredMemberId, AuthoredOwner, SourceProvider, SourceSnapshot, + SourceVersion, +}; +use std::collections::BTreeMap; + +fn snapshot(identity: &str, source: &str) -> SourceSnapshot { + SourceSnapshot::new(identity, "memory:/", source) +} + +#[derive(Default)] +struct MemoryProvider { + sources: BTreeMap<(String, String), SourceSnapshot>, +} + +impl MemoryProvider { + fn insert(&mut self, from: &str, location: &str, source: SourceSnapshot) { + self.sources + .insert((from.to_owned(), location.to_owned()), source); + } +} + +impl SourceProvider for MemoryProvider { + fn resolve( + &mut self, + containing: &SourceSnapshot, + location: &str, + ) -> Result { + self.sources + .get(&(containing.identity().to_owned(), location.to_owned())) + .cloned() + .ok_or_else(|| "not found".into()) + } +} + +#[test] +fn two_component_uses_give_one_member_distinct_occurrence_addresses() { + let source = r##" + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = n0_xml_source::materialize(snapshot("entry", source), &mut provider).unwrap(); + assert_eq!( + output.program.unit("entry").unwrap().version(), + SourceVersion::Version4 + ); + assert_eq!( + output.addresses().len(), + output.document.len() - 1, + "only the implicit document root is not authored" + ); + + let mut bodies = output + .addresses() + .filter(|(address, _)| address.member.id == AuthoredMemberId::Id("badge-body".into())) + .map(|(address, node)| (address.clone(), node)) + .collect::>(); + bodies.sort_by(|a, b| a.0.cmp(&b.0)); + assert_eq!(bodies.len(), 2); + assert_ne!(bodies[0].0, bodies[1].0); + assert_ne!(bodies[0].1.id(), bodies[1].1.id()); + assert_eq!( + bodies + .iter() + .map(|(address, _)| address.use_path[0].id.as_str()) + .collect::>(), + ["first-badge", "second-badge"] + ); + for (address, node) in bodies { + assert_eq!( + address.member.owner, + AuthoredOwner::Component(n0_xml_source::ComponentIdentity { + source: "entry".into(), + id: "badge".into(), + }) + ); + assert_eq!(output.node_for_address(&address).unwrap(), node); + assert_eq!(output.address_for_node(node).unwrap(), &address); + } +} + +#[test] +fn authored_root_id_is_distinct_from_the_structural_component_root() { + let source = r##" + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = n0_xml_source::materialize(snapshot("entry", source), &mut provider).unwrap(); + let owner = AuthoredOwner::Component(n0_xml_source::ComponentIdentity { + source: "entry".into(), + id: "card".into(), + }); + let structural_root = output + .addresses() + .find(|(address, _)| { + address.member.owner == owner && address.member.id == AuthoredMemberId::ComponentRoot + }) + .unwrap(); + let authored_root = output + .addresses() + .find(|(address, _)| { + address.member.owner == owner + && address.member.id == AuthoredMemberId::Id("root".into()) + }) + .unwrap(); + + assert_ne!(structural_root.0, authored_root.0); + assert_ne!(structural_root.1, authored_root.1); + assert_eq!(structural_root.0.use_path, authored_root.0.use_path); + assert_eq!( + authored_root + .0 + .use_path + .iter() + .map(|occurrence| occurrence.id.as_str()) + .collect::>(), + ["card-use"] + ); +} + +#[test] +fn nested_uses_and_slot_assignments_keep_path_order_and_authored_owner() { + let source = r##" + + + + + + + + + + + + + + +"##; + let mut provider = MemoryProvider::default(); + let output = n0_xml_source::materialize(snapshot("entry", source), &mut provider).unwrap(); + + let caller_rect = output + .addresses() + .find(|(address, _)| address.member.id == AuthoredMemberId::Id("caller-rect".into())) + .unwrap(); + assert_eq!( + caller_rect.0.member.owner, + AuthoredOwner::Scene { + source: "entry".into() + } + ); + assert_eq!( + caller_rect + .0 + .use_path + .iter() + .map(|occurrence| occurrence.id.as_str()) + .collect::>(), + ["shell-use"] + ); + + let leaf_mark = output + .addresses() + .find(|(address, _)| address.member.id == AuthoredMemberId::Id("leaf-mark".into())) + .unwrap(); + assert_eq!( + leaf_mark.0.member.owner, + AuthoredOwner::Component(n0_xml_source::ComponentIdentity { + source: "entry".into(), + id: "leaf".into(), + }) + ); + assert_eq!( + leaf_mark + .0 + .use_path + .iter() + .map(|occurrence| occurrence.id.as_str()) + .collect::>(), + ["shell-use", "nested-leaf-use"] + ); +} + +#[test] +fn version4_ids_are_required_well_formed_and_unique_per_lexical_owner() { + let cases = [ + ( + r##""##, + "requires a durable", + true, + ), + ( + r##""##, + "requires a durable", + true, + ), + ( + r##""##, + "lowercase kebab-case", + true, + ), + ( + r##""##, + "duplicate Version 4 member/use id `same`", + true, + ), + ( + r##""##, + "unknown attribute `id`", + false, + ), + ]; + for (source, expected, local_id_error) in cases { + let error = n0_xml_source::parse_source(snapshot("bad", source)).unwrap_err(); + assert!(error.message.contains(expected), "{error}"); + if local_id_error { + assert!( + error.span.is_some(), + "Version 4 id errors retain a local span" + ); + } + } + + let owner_local = r##" + + + + +"##; + n0_xml_source::parse_source(snapshot("ok", owner_local)).unwrap(); +} + +#[test] +fn versions_zero_through_three_keep_their_exact_id_posture() { + for version in ["1", "2", "3"] { + let source = format!( + r##""## + ); + n0_xml_source::parse_source(snapshot("old", &source)).unwrap(); + } + + let old_with_id = r##""##; + let error = n0_xml_source::parse_source(snapshot("old", old_with_id)).unwrap_err(); + assert!(error.message.contains("unknown attribute `id`"), "{error}"); +} + +#[test] +fn version4_rejects_legacy_component_sources_before_materialization() { + for target_version in ["1", "2", "3"] { + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./library", + snapshot( + &format!("library-v{target_version}"), + &format!( + r##""## + ), + ), + ); + let entry = r##" + + + +"##; + let error = + n0_xml_source::materialize(snapshot("entry", entry), &mut provider).unwrap_err(); + assert_eq!(error.phase, n0_xml_source::ErrorPhase::Link); + assert!( + error.message.contains(&format!( + "Version 4 source cannot link Version {target_version} component source" + )), + "{error}" + ); + } +} + +#[test] +fn version4_to_version4_slot_assignment_has_a_complete_address_closure() { + let mut provider = MemoryProvider::default(); + provider.insert( + "entry", + "./library", + snapshot( + "library-v4", + r##" + + + + + +"##, + ), + ); + let entry = r##" + + + + + + + +"##; + let output = n0_xml_source::materialize(snapshot("entry", entry), &mut provider).unwrap(); + assert_eq!( + output.program.unit("library-v4").unwrap().version(), + SourceVersion::Version4 + ); + assert_eq!(output.addresses().len() + 1, output.document.len()); + for (address, node) in output.addresses() { + assert_eq!(output.node_for_address(address).unwrap(), node); + assert_eq!(output.address_for_node(node).unwrap(), address); + } + + let assigned = output + .addresses() + .find(|(address, _)| address.member.id == AuthoredMemberId::Id("assigned-rect".into())) + .unwrap(); + assert_eq!( + assigned + .0 + .use_path + .iter() + .map(|occurrence| occurrence.id.as_str()) + .collect::>(), + ["host-use"] + ); +} + +#[test] +fn older_nodes_are_unaddressed_and_removed_version4_nodes_fail_closed() { + let old = r##""##; + let mut provider = MemoryProvider::default(); + let old = n0_xml_source::materialize(snapshot("old", old), &mut provider).unwrap(); + assert_eq!(old.addresses().len(), 0); + let old_root = old.document.get(old.document.root).children[0]; + let old_key = old.document.key_of(old_root).unwrap(); + assert!(matches!( + old.address_for_node(old_key), + Err(AddressLookupError::NodeHasNoDurableAddress { .. }) + )); + + let source = r##""##; + let mut output = n0_xml_source::materialize(snapshot("new", source), &mut provider).unwrap(); + let (address, key) = output + .addresses() + .find(|(address, _)| address.member.id == AuthoredMemberId::Id("member".into())) + .map(|(address, key)| (address.clone(), key)) + .unwrap(); + let live_before = output.addresses().len(); + output.document.remove_subtree(key.id()); + assert_eq!(output.addresses().len(), live_before - 1); + assert!(output + .addresses() + .all(|(live_address, live_node)| live_address != &address && live_node != key)); + assert!(matches!( + output.node_for_address(&address), + Err(AddressLookupError::StaleAddress { .. }) + )); + assert!(matches!( + output.address_for_node(key), + Err(AddressLookupError::UnknownNode { .. }) + )); +} diff --git a/crates/n0-model/tests/n0_xml_stroke_widths.rs b/crates/n0-model/tests/n0_xml_stroke_widths.rs new file mode 100644 index 00000000..c4ea7a5b --- /dev/null +++ b/crates/n0-model/tests/n0_xml_stroke_widths.rs @@ -0,0 +1,212 @@ +//! Draft 0 rectangular stroke-width grammar and model projection. + +use n0_model::model::*; +use n0_model::n0_xml; + +fn named(doc: &Document, name: &str) -> NodeId { + (0..doc.capacity() as NodeId) + .find(|id| { + doc.get_opt(*id) + .is_some_and(|node| node.header.name.as_deref() == Some(name)) + }) + .expect("named node") +} + +fn rect_document(node_attributes: &str, stroke_attributes: &str) -> String { + format!( + r##""## + ) +} + +#[test] +fn four_widths_project_to_the_production_shaped_model_and_round_trip() { + let source = rect_document("", r#"width="2 4 6 8" align="outside" dash-array="5 3""#); + let doc = n0_xml::parse(&source).unwrap(); + let stroke = &doc.get(named(&doc, "target")).strokes[0]; + assert_eq!( + stroke.width, + StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 2.0, + stroke_right_width: 4.0, + stroke_bottom_width: 6.0, + stroke_left_width: 8.0, + }) + ); + assert_eq!(stroke.dash_array.as_deref(), Some(&[5.0, 3.0][..])); + + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains(r#"width="2 4 6 8""#), "{printed}"); + assert_eq!( + n0_xml::print(&n0_xml::parse(&printed).unwrap()).unwrap(), + printed + ); +} + +#[test] +fn container_strokes_accept_the_same_four_side_geometry() { + let source = r##""##; + let doc = n0_xml::parse(source).unwrap(); + assert_eq!( + doc.get(named(&doc, "target")).strokes[0].width, + StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 3.0, + stroke_right_width: 5.0, + stroke_bottom_width: 7.0, + stroke_left_width: 9.0, + }) + ); + assert!(n0_xml::print(&doc).unwrap().contains(r#"width="3 5 7 9""#)); +} + +#[test] +fn equal_and_zero_side_lists_normalize_to_the_shortest_scalar_form() { + let equal = n0_xml::parse(&rect_document("", r#"width="7 7 7 7""#)).unwrap(); + let stroke = &equal.get(named(&equal, "target")).strokes[0]; + assert_eq!(stroke.width, StrokeWidth::Uniform(7.0)); + let printed = n0_xml::print(&equal).unwrap(); + assert!(printed.contains(r#"width="7""#), "{printed}"); + assert!(!printed.contains(r#"width="7 7 7 7""#), "{printed}"); + + let zero = n0_xml::parse(&rect_document("", r#"width="0 0 0 0""#)).unwrap(); + let stroke = &zero.get(named(&zero, "target")).strokes[0]; + assert_eq!(stroke.width, StrokeWidth::None); + let printed = n0_xml::print(&zero).unwrap(); + assert!(printed.contains(r#"width="0""#), "{printed}"); + + let default = n0_xml::parse(&rect_document("", r#"width="1 1 1 1""#)).unwrap(); + let printed = n0_xml::print(&default).unwrap(); + assert!(!printed.contains(""#; + let doc = n0_xml::parse(source).unwrap(); + let printed = n0_xml::print(&doc).unwrap(); + assert!( + printed.contains(r#""#), + "{printed}" + ); + assert_eq!(n0_xml::parse(&printed).unwrap(), doc); + + let default_alias = r#""#; + let error = n0_xml::parse(default_alias).unwrap_err(); + assert!( + error.0.contains("indistinguishable from omission"), + "{error}" + ); +} + +#[test] +fn writer_normalizes_equivalent_programmatic_width_variants() { + let mut doc = n0_xml::parse(&rect_document("", r#"width="3""#)).unwrap(); + let target = named(&doc, "target"); + doc.get_mut(target).strokes[0].width = + StrokeWidth::Rectangular(RectangularStrokeWidth::all(3.0)); + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains(r#"width="3""#), "{printed}"); + + doc.get_mut(target).strokes[0].width = StrokeWidth::Uniform(0.0); + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains(r#"width="0""#), "{printed}"); +} + +#[test] +fn malformed_or_negative_side_lists_are_rejected() { + let cases = [ + (r#"width="""#, "takes 1 or exactly 4 numbers"), + (r#"width="1 2""#, "got 2"), + (r#"width="1 2 3""#, "got 3"), + (r#"width="1 2 3 4 5""#, "got 5"), + (r#"width="1 -2 3 4""#, "stroke width right"), + (r#"width="1 NaN 3 4""#, "non-finite"), + ]; + for (attributes, expected) in cases { + let error = n0_xml::parse(&rect_document("", attributes)).unwrap_err(); + assert!(error.0.contains(expected), "{attributes}: {error}"); + } +} + +#[test] +fn four_value_syntax_is_rectangular_only_even_when_the_values_are_equal() { + let targets = [ + r##""##, + r##""##, + r##"x"##, + ]; + for target in targets { + let source = format!(r#"{target}"#); + let error = n0_xml::parse(&source).unwrap_err(); + assert!( + error.0.contains("four-value stroke width is valid only"), + "{error}" + ); + } +} + +#[test] +fn per_side_width_rejects_renderer_states_that_production_cannot_preserve() { + let cases = [ + ( + "corner-radius=\"8\" corner-smoothing=\"0.25\"", + r#"width="1 2 3 4""#, + "corner-smoothing", + ), + ("", r#"width="1 2 3 4" join="round""#, "default join"), + ( + "", + r#"width="1 2 3 4" miter-limit="6""#, + "miter-limit=\"4\"", + ), + ]; + for (node_attributes, stroke_attributes, expected) in cases { + let error = n0_xml::parse(&rect_document(node_attributes, stroke_attributes)).unwrap_err(); + assert!(error.0.contains(expected), "{error}"); + } +} + +#[test] +fn writer_applies_the_same_cross_geometry_gates() { + let mut doc = n0_xml::parse(&rect_document("", r#"width="1 2 3 4""#)).unwrap(); + let target = named(&doc, "target"); + doc.get_mut(target).corner_smoothing = CornerSmoothing(0.5); + let error = n0_xml::print(&doc).unwrap_err(); + assert!(error.to_string().contains("corner-smoothing"), "{error}"); + + doc.get_mut(target).corner_smoothing = CornerSmoothing::default(); + doc.get_mut(target).strokes[0].join = StrokeJoin::Bevel; + let error = n0_xml::print(&doc).unwrap_err(); + assert!(error.to_string().contains("default join"), "{error}"); + + let source = r##""##; + let mut ellipse = n0_xml::parse(source).unwrap(); + let target = named(&ellipse, "target"); + ellipse.get_mut(target).strokes[0].width = StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 1.0, + stroke_right_width: 2.0, + stroke_bottom_width: 3.0, + stroke_left_width: 4.0, + }); + let error = n0_xml::print(&ellipse).unwrap_err(); + assert!( + error.to_string().contains("cannot carry per-side"), + "{error}" + ); +} + +#[test] +fn writer_reports_the_nonfinite_side_before_tree_integrity() { + let mut doc = n0_xml::parse(&rect_document("", r#"width="1 2 3 4""#)).unwrap(); + let target = named(&doc, "target"); + let StrokeWidth::Rectangular(widths) = &mut doc.get_mut(target).strokes[0].width else { + panic!("rectangular width") + }; + widths.stroke_right_width = f32::NAN; + + let error = n0_xml::print(&doc).unwrap_err(); + assert!( + error.to_string().contains("width right must be finite"), + "{error}" + ); + assert!(!error.to_string().contains("scene tree"), "{error}"); +} diff --git a/crates/n0-model/tests/n0_xml_strokes.rs b/crates/n0-model/tests/n0_xml_strokes.rs new file mode 100644 index 00000000..ea6b760c --- /dev/null +++ b/crates/n0-model/tests/n0_xml_strokes.rs @@ -0,0 +1,340 @@ +//! Draft 0 stroke-channel contract: repeated geometry applications, target- +//! specific attributes, ordered paints, canonical defaults, and strict errors. + +use n0_model::model::*; +use n0_model::n0_xml::{self, PrintError}; + +fn named(doc: &Document, name: &str) -> NodeId { + (0..doc.capacity() as NodeId) + .find(|id| { + doc.get_opt(*id) + .is_some_and(|node| node.header.name.as_deref() == Some(name)) + }) + .expect("named node") +} + +#[test] +fn repeated_strokes_preserve_geometry_paint_and_source_order() { + let source = r##" + + + + + + + + + + + + + + + + +"##; + let doc = n0_xml::parse(source).expect("repeated strokes parse"); + let road = doc.get(named(&doc, "road")); + assert_eq!(road.strokes.len(), 2); + + let bottom = &road.strokes[0]; + assert_eq!(bottom.width, StrokeWidth::Uniform(12.0)); + assert_eq!(bottom.align, StrokeAlign::Outside); + assert_eq!(bottom.join, StrokeJoin::Round); + assert_eq!(bottom.miter_limit, 6.0); + assert_eq!( + bottom.dash_array.as_deref(), + Some(&[4.0, 2.0, 1.0, 4.0, 2.0, 1.0][..]), + "odd dash arrays repeat once" + ); + assert!(matches!(bottom.paints[0], Paint::Solid(_))); + + let top = &road.strokes[1]; + assert_eq!(top.width, StrokeWidth::Uniform(3.0)); + assert_eq!(top.align, StrokeAlign::Inside); + assert_eq!(top.paints.len(), 2); + assert!(matches!(top.paints[0], Paint::Solid(_))); + assert!(matches!(top.paints[1], Paint::LinearGradient(_))); + + let printed = n0_xml::print(&doc).expect("repeated strokes print"); + let bottom_at = printed.find("width=\"12\"").unwrap(); + let top_at = printed.find("width=\"3\"").unwrap(); + assert!(bottom_at < top_at, "{printed}"); + assert!(printed.contains("dash-array=\"4 2 1 4 2 1\""), "{printed}"); + assert!(printed.contains(" + + + + x + +"##; + let doc = n0_xml::parse(source).unwrap(); + for name in ["rect", "ellipse", "text"] { + let stroke = &doc.get(named(&doc, name)).strokes[0]; + assert_eq!(stroke.width, StrokeWidth::Uniform(1.0), "{name}"); + assert_eq!(stroke.align, StrokeAlign::Inside, "{name}"); + assert_eq!(stroke.cap, StrokeCap::Butt, "{name}"); + assert_eq!(stroke.join, StrokeJoin::Miter, "{name}"); + assert_eq!(stroke.miter_limit, 4.0, "{name}"); + assert_eq!(stroke.dash_array, None, "{name}"); + } + assert_eq!( + doc.get(named(&doc, "line")).strokes[0].align, + StrokeAlign::Center + ); + + let printed = n0_xml::print(&doc).unwrap(); + assert!( + !printed.contains("width=\"1\""), + "defaults are omitted: {printed}" + ); + assert!(!printed.contains("align=\"inside\""), "{printed}"); + assert!(!printed.contains("align=\"center\""), "{printed}"); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn enumerated_stroke_geometry_values_match_the_model_vocabulary() { + for (value, expected) in [ + ("inside", StrokeAlign::Inside), + ("center", StrokeAlign::Center), + ("outside", StrokeAlign::Outside), + ] { + let source = format!( + r##""## + ); + let doc = n0_xml::parse(&source).unwrap(); + assert_eq!(doc.get(named(&doc, "r")).strokes[0].align, expected); + } + + for (value, expected) in [ + ("butt", StrokeCap::Butt), + ("round", StrokeCap::Round), + ("square", StrokeCap::Square), + ] { + let source = format!( + r##""## + ); + let doc = n0_xml::parse(&source).unwrap(); + assert_eq!(doc.get(named(&doc, "line")).strokes[0].cap, expected); + } + + for (value, expected) in [ + ("miter", StrokeJoin::Miter), + ("round", StrokeJoin::Round), + ("bevel", StrokeJoin::Bevel), + ] { + let source = format!( + r##""## + ); + let doc = n0_xml::parse(&source).unwrap(); + assert_eq!(doc.get(named(&doc, "r")).strokes[0].join, expected); + } +} + +#[test] +fn default_empty_stroke_is_invalid_but_non_default_empty_geometry_survives() { + for source in [ + r#""#, + r#""#, + ] { + let error = n0_xml::parse(source).unwrap_err(); + assert!(error.to_string().contains("default empty"), "{error}"); + assert!( + error + .to_string() + .contains("indistinguishable from omission"), + "{error}" + ); + } + + let doc = n0_xml::parse( + r#""#, + ) + .expect("non-default empty geometry remains authored state"); + let stroke = &doc.get(named(&doc, "dormant")).strokes[0]; + assert!(stroke.paints.is_empty()); + assert_eq!(stroke.width, StrokeWidth::Uniform(2.0)); + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains(""), "{printed}"); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn target_specific_stroke_attributes_and_numeric_domains_are_strict() { + let cases = [ + ( + r##""##, + "cap", + ), + ( + r##""##, + "join", + ), + ( + r##"x"##, + "dash-array", + ), + ( + r##""##, + "center", + ), + ( + r##""##, + "join", + ), + ( + r##""##, + "non-negative", + ), + ( + r##""##, + "greater than zero", + ), + ( + r##""##, + "dash-array", + ), + ( + r##""##, + "all zero", + ), + ( + r##""##, + "non-negative", + ), + ( + r##""##, + "group", + ), + ( + r##""##, + "lens", + ), + ]; + + for (node, expected) in cases { + let source = format!("{node}"); + let error = n0_xml::parse(&source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}` for {node}" + ); + } +} + +#[test] +fn stroke_structure_and_property_order_are_strict() { + let cases = [ + ( + r##""##, + "nested ", + ), + ( + r##""##, + "", + ), + ( + r##"x"##, + "must appear before", + ), + ( + r##""##, + "nested ", + ), + ]; + for (source, expected) in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}`" + ); + } +} + +#[test] +fn text_content_is_exact_around_fill_and_stroke_properties() { + let source = r##" + + hello +world "##; + let doc = n0_xml::parse(source).unwrap(); + let label = doc.get(named(&doc, "label")); + let Payload::Text { content, .. } = &label.payload else { + panic!("text payload") + }; + assert_eq!(content, " hello\nworld "); + assert_eq!(label.strokes.len(), 1); + let printed = n0_xml::print(&doc).unwrap(); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn writer_omits_default_empty_stroke_and_refuses_invalid_stroke_state() { + let mut doc = n0_xml::parse( + r#""#, + ) + .unwrap(); + let r = named(&doc, "r"); + let payload = doc.get(r).payload.clone(); + doc.get_mut(r) + .strokes + .push(Stroke::default_for(&payload).unwrap()); + let printed = n0_xml::print(&doc).expect("default empty stroke normalizes to omission"); + assert!(!printed.contains(""##, + ) + .unwrap(); + let line_id = named(&line, "line"); + line.get_mut(line_id).strokes[0].align = StrokeAlign::Inside; + assert!(matches!( + n0_xml::print(&line), + Err(PrintError::InvalidDocument(message)) if message.contains("line") && message.contains("center") + )); +} + +#[test] +fn writer_normalizes_programmatic_odd_dash_cycles_without_losing_semantics() { + let mut doc = n0_xml::parse( + r##""##, + ) + .unwrap(); + let r = named(&doc, "r"); + doc.get_mut(r).strokes[0].dash_array = Some(vec![3.0, 2.0, 1.0]); + + let printed = n0_xml::print(&doc).expect("odd dash cycles have an even canonical form"); + assert!(printed.contains("dash-array=\"3 2 1 3 2 1\""), "{printed}"); + let reparsed = n0_xml::parse(&printed).unwrap(); + assert_eq!( + reparsed.get(named(&reparsed, "r")).strokes[0] + .dash_array + .as_deref(), + Some(&[3.0, 2.0, 1.0, 3.0, 2.0, 1.0][..]) + ); +} diff --git a/crates/n0-model/tests/n0_xml_suite.rs b/crates/n0-model/tests/n0_xml_suite.rs new file mode 100644 index 00000000..3fa6d06c --- /dev/null +++ b/crates/n0-model/tests/n0_xml_suite.rs @@ -0,0 +1,817 @@ +//! Draft 0 `.n0.xml` producer contract: structural envelope, canonical +//! vocabulary, primitive-local composition, strict roots, and historical-TextIr compatibility. + +use n0_model::model::*; +use n0_model::n0_xml::{self, PrintError}; +use n0_model::resolve::{resolve, ResolveOptions}; +use std::collections::BTreeMap; + +const COMPOSED: &str = r#" + + + + content + + + +"#; + +/// The envelope is not a node. The model keeps its canonical viewport root, +/// then the authored container → rect → text tree below it. A primitive owns +/// a free-positioned child coordinate space without gaining layout behavior. +#[test] +fn draft0_preserves_primitive_children_and_local_geometry() { + let doc = n0_xml::parse(COMPOSED).expect("Draft 0 parses"); + let document_root = doc.get(doc.root); + + assert_eq!(doc.root, 0); + assert_eq!( + document_root.header.x, + AxisBinding::Span { + start: 0.0, + end: 0.0 + } + ); + assert_eq!( + document_root.header.y, + AxisBinding::Span { + start: 0.0, + end: 0.0 + } + ); + assert_eq!(document_root.children.len(), 1); + + let container = document_root.children[0]; + let shape = doc.get(container).children[0]; + let text = doc.get(shape).children[0]; + assert!(matches!(doc.get(container).payload, Payload::Frame { .. })); + assert!(matches!(doc.get(shape).payload, Payload::Shape { .. })); + assert!(matches!(doc.get(text).payload, Payload::Text { .. })); + + let resolved = resolve( + &doc, + &ResolveOptions { + viewport: (1024.0, 768.0), + ..Default::default() + }, + ); + assert_eq!(resolved.xywh(doc.root), (0.0, 0.0, 1024.0, 768.0)); + assert_eq!(resolved.xywh(container), (0.0, 0.0, 800.0, 450.0)); + assert_eq!(resolved.xywh(shape), (100.0, 50.0, 200.0, 100.0)); + assert_eq!(resolved.box_of(text).x, 10.0); + assert_eq!(resolved.box_of(text).y, 12.0); + let world = resolved.world_of(text); + assert_eq!((world.e, world.f), (110.0, 62.0)); +} + +/// Canonical output names the real source format rather than the historical-TextIr +/// experiment, and normalized parse → print → parse remains a fixpoint. +#[test] +fn draft0_canonical_print_is_a_semantic_fixpoint() { + let doc = n0_xml::parse(COMPOSED).unwrap(); + let printed = n0_xml::print(&doc).unwrap(); + + assert!(printed.starts_with("\n")); + assert!(printed.contains("")); + assert!(printed.contains("")); + assert!(!printed.contains(" + + + + + content + + + +"#; + let doc = n0_xml::parse(source).expect("direct primitives parse"); + let container = doc.get(doc.root).children[0]; + let rect = doc.get(container).children[0]; + let children = &doc.get(rect).children; + + assert!(matches!( + doc.get(rect).payload, + Payload::Shape { + desc: ShapeDesc::Rect + } + )); + assert_eq!(children.len(), 3); + assert!(matches!( + doc.get(children[0]).payload, + Payload::Shape { + desc: ShapeDesc::Ellipse + } + )); + assert!(matches!( + doc.get(children[1]).payload, + Payload::Shape { + desc: ShapeDesc::Line + } + )); + assert!(matches!(doc.get(children[2]).payload, Payload::Text { .. })); + assert_eq!(doc.get(children[0]).header.name.as_deref(), Some("ellipse")); + assert_eq!(doc.get(children[1]).header.height, SizeIntent::Fixed(0.0)); + + let printed = n0_xml::print(&doc).expect("direct primitives print"); + assert!(printed.contains("")); + assert!(printed.contains("")); + assert!(printed.contains("")); + assert!(!printed.contains(""#, + ) + .unwrap_err(); + assert!(reserved.to_string().contains(" is reserved")); + + let kind_alias = n0_xml::parse( + r#""#, + ) + .unwrap_err(); + assert!(kind_alias.to_string().contains("unknown attribute `kind`")); + + for primitive in [ + r#""#, + r#""#, + r#""#, + ] { + let source = format!("{primitive}"); + let error = n0_model::textir::parse(&source).unwrap_err(); + assert!( + error.to_string().contains("unknown element"), + "historical TextIr must reject {primitive}: {error}" + ); + } + + let historical = n0_model::textir::parse( + r#""#, + ) + .expect("historical TextIr keeps "); + let printed = n0_model::textir::print(&historical); + assert!(printed.contains(r#""#)); +} + +#[test] +fn n0_xml_rejects_unreserved_native_animation_syntax() { + for source in [ + r#""#, + r#""#, + r#""#, + ] { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains("unknown"), + "native animation syntax must fail explicitly: {error}" + ); + } +} + +#[test] +fn canonical_print_escapes_attribute_values() { + let source = r#""#; + let doc = n0_xml::parse(source).unwrap(); + let printed = n0_xml::print(&doc).unwrap(); + assert!(printed.contains("name=\"A & "B"\"")); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn draft0_uses_long_constraint_vocabulary() { + let source = r#" + + + + + +"#; + let doc = n0_xml::parse(source).unwrap(); + let container = doc.get(doc.root).children[0]; + let shape = doc.get(container).children[0]; + assert_eq!( + resolve(&doc, &ResolveOptions::default()).box_of(shape).h, + 30.0 + ); + let printed = n0_xml::print(&doc).unwrap(); + for spelling in [ + "min-width=\"10\"", + "max-width=\"40\"", + "min-height=\"15\"", + "max-height=\"50\"", + "aspect-ratio=\"2:3\"", + "flip-y=\"true\"", + ] { + assert!(printed.contains(spelling), "missing {spelling}:\n{printed}"); + } + assert!(!printed.contains(" min-w=\"")); + assert!(!printed.contains(" aspect=\"")); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); +} + +#[test] +fn direct_text_whitespace_is_preserved() { + let source = + " hello\nworld "; + let doc = n0_xml::parse(source).unwrap(); + let container = doc.get(doc.root).children[0]; + let text = doc.get(container).children[0]; + match &doc.get(text).payload { + Payload::Text { content, .. } => assert_eq!(content, " hello\nworld "), + payload => panic!("expected text, got {payload:?}"), + } + let printed = n0_xml::print(&doc).unwrap(); + assert_eq!(doc, n0_xml::parse(&printed).unwrap()); + + let historical = + n0_model::textir::parse(" hello\nworld ") + .unwrap(); + let historical_text = historical.get(historical.root).children[0]; + match &historical.get(historical_text).payload { + Payload::Text { content, .. } => assert_eq!(content, "hello\nworld"), + payload => panic!("expected text, got {payload:?}"), + } +} + +#[test] +fn booleans_are_exactly_true_or_false() { + let cases = [ + r#""#, + r#""#, + r#""#, + r#""#, + ]; + for source in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!(error.to_string().contains("exactly `true` or `false`")); + } +} + +#[test] +fn numeric_domains_are_checked_at_parse_time() { + let cases = [ + ( + r#""#, + "width must be non-negative", + ), + ( + r#"x"#, + "font-size must be greater than zero", + ), + ( + r#"x"#, + "opacity must be between 0 and 1", + ), + ( + r#"x"#, + "min-width must be non-negative", + ), + ( + r#"x"#, + "aspect-ratio must be greater than zero", + ), + ]; + for (source, expected) in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}`" + ); + } +} + +#[test] +fn grow_gap_and_padding_are_non_negative_only_in_draft0() { + let cases = [ + ( + r#""#, + "grow must be non-negative", + ), + ( + r#""#, + "gap must be non-negative", + ), + ( + r#""#, + "gap must be non-negative", + ), + ( + r#""#, + "padding must be non-negative", + ), + ]; + for (source, expected) in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}`" + ); + } + + n0_model::textir::parse( + r#""#, + ) + .expect("historical TextIr keeps its prior numeric behavior"); +} + +#[test] +fn flex_child_attributes_are_context_strict() { + let rejected = [ + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + ]; + for source in rejected { + assert!(n0_xml::parse(source).is_err(), "must reject: {source}"); + } + + for source in [ + r#""#, + r#""#, + ] { + n0_xml::parse(source).expect("applicable flex attributes accepted"); + } + + n0_model::textir::parse( + r#""#, + ) + .expect("historical TextIr retains resolver-reported applicability"); +} + +#[test] +fn strict_lens_ops_reject_empty_arguments() { + for ops in [ + "translate(1,,2)", + "translate(1,2", + "translate(1,2)scale(2)", + "translate(1,2), scale(2)", + "translate(1,2))", + ] { + let strict = + format!(""); + assert!(n0_xml::parse(&strict).is_err(), "must reject `{ops}`"); + } + + n0_model::textir::parse(r#""#) + .expect("historical TextIr retains empty-argument filtering"); +} + +#[test] +fn xml_declaration_is_single_and_before_the_envelope() { + for valid in [ + r#""#, + r#""#, + r#""#, + ] { + n0_xml::parse(valid).expect("supported leading declaration is accepted"); + } + + for source in [ + r#""#, + r#""#, + r#""#, + r#""#, + " \n", + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + ] { + assert!(n0_xml::parse(source).is_err(), "must reject: {source}"); + } + + n0_model::textir::parse(r#""#) + .expect("historical TextIr continues to ignore declaration contents"); +} + +#[test] +fn authored_root_and_shape_constraints_are_structural() { + for attr in ["flow=\"absolute\"", "grow=\"1\"", "align=\"center\""] { + let source = format!(""); + let error = n0_xml::parse(&source).unwrap_err(); + assert!(error.to_string().contains("authored root ")); + } + + for source in [ + r#""#, + r#""#, + r#""#, + ] { + let doc = n0_xml::parse(source).expect("Span supplies the primitive axis"); + n0_xml::print(&doc).expect("accepted primitive sizing prints and reparses"); + } + + let cases = [ + ( + r#""#, + "require both axes supplied", + ), + ( + r#""#, + "require both axes supplied", + ), + ( + r#""#, + "must not declare height", + ), + ( + r#""#, + "must not declare min-height/max-height", + ), + ( + r#""#, + "must not declare a y Span", + ), + ( + r#""#, + "span x binding cannot also declare width", + ), + ( + r#"x"#, + "span x binding cannot also declare width/min-width/max-width", + ), + ( + r#"x"#, + "span y binding cannot also declare height/min-height/max-height", + ), + ( + r#""#, + "derived box", + ), + ( + r#""#, + "derived origin and cannot use Span", + ), + ( + r#""#, + "derived origin and cannot use Span", + ), + ( + r#""#, + "require both axes supplied", + ), + ( + r#""#, + "require both axes supplied", + ), + ( + r#""#, + "must not declare aspect-ratio", + ), + ( + r#""#, + "only valid on and ", + ), + ]; + for (source, expected) in cases { + let error = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}`" + ); + } +} + +/// The authored scene is a free child of the implicit, definite viewport +/// frame. Root bindings therefore use the ordinary binding model; explicit +/// Span keeps viewport-fill intent inspectable without changing `auto`/hug. +#[test] +fn authored_root_bindings_resolve_against_the_viewport_and_roundtrip() { + let source = r#" + + + + + +"#; + let doc = n0_xml::parse(source).expect("root bindings parse"); + let printed = n0_xml::print(&doc).expect("root bindings print"); + let reparsed = n0_xml::parse(&printed).expect("root bindings reparse"); + assert_eq!(doc, reparsed); + + let root = doc.get(doc.root).children[0]; + let nested = doc.get(root).children[0]; + assert_eq!( + doc.get(root).header.x, + AxisBinding::Span { + start: 12.0, + end: 20.0 + } + ); + assert_eq!( + doc.get(root).header.y, + AxisBinding::Span { + start: 8.0, + end: 16.0 + } + ); + assert!(printed.contains( + "" + )); + assert!(!printed.contains("x=\"span 12 20\" y=\"span 8 16\" width=")); + assert!(printed.contains("")); + + for (viewport, expected_root, expected_nested) in [ + ( + (1280.0, 720.0), + (12.0, 8.0, 1248.0, 696.0), + (5.0, 3.0, 1236.0, 684.0), + ), + ( + (390.0, 844.0), + (12.0, 8.0, 358.0, 820.0), + (5.0, 3.0, 346.0, 808.0), + ), + ] { + let resolved = resolve( + &doc, + &ResolveOptions { + viewport, + ..Default::default() + }, + ); + assert!(resolved.reports.is_empty(), "{:?}", resolved.reports); + assert_eq!(resolved.xywh(root), expected_root); + assert_eq!(resolved.xywh(nested), expected_nested); + } + + for (bindings, expected) in [ + (r#"x="10" y="12""#, (10.0, 12.0, 100.0, 50.0)), + (r#"x="end 10" y="end 12""#, (1170.0, 658.0, 100.0, 50.0)), + (r#"x="center -5" y="center 6""#, (585.0, 341.0, 100.0, 50.0)), + ] { + let source = format!( + "" + ); + let doc = n0_xml::parse(&source).expect("root pin binding parses"); + let root = doc.get(doc.root).children[0]; + let resolved = resolve( + &doc, + &ResolveOptions { + viewport: (1280.0, 720.0), + ..Default::default() + }, + ); + assert!(resolved.reports.is_empty(), "{:?}", resolved.reports); + assert_eq!(resolved.xywh(root), expected); + let printed = n0_xml::print(&doc).expect("root pin binding prints"); + assert_eq!( + doc, + n0_xml::parse(&printed).expect("root pin binding reparses") + ); + } +} + +#[test] +fn fill_shorthand_normalizes_and_is_not_valid_on_derived_kinds() { + for fill in ["112233", "#GG0000", "#12"] { + let source = format!( + "" + ); + let error = n0_xml::parse(&source).unwrap_err(); + assert!(error.to_string().contains("#RGB or #RRGGBB")); + } + let group = r##""##; + assert!(n0_xml::parse(group) + .unwrap_err() + .to_string() + .contains("fill is not valid")); + + let valid = r##""##; + let printed = n0_xml::print(&n0_xml::parse(valid).unwrap()).unwrap(); + assert!(printed.contains("fill=\"#AABBCC\"")); + assert!(!printed.contains("")); +} + +/// Draft 0 has one vocabulary. Historical spellings remain available only +/// through the explicitly historical TextIr surface. +#[test] +fn historical_vocabulary_is_textir_only() { + for source in [ + r#""#, + r#""#, + r#""#, + ] { + assert!(n0_xml::parse(source).is_err(), "Draft 0 rejected: {source}"); + } + + let historical = n0_model::textir::parse(r#""#) + .expect("historical TextIr parser remains available"); + assert!(n0_model::textir::print(&historical).starts_with(""#, + "unsupported", + ), + ( + r#""#, + "requires version", + ), + ( + r#""#, + "multiple render roots", + ), + (r#""#, "exactly one render root"), + (r#""#, "must be inside"), + ( + r#""#, + "unknown attribute `w`", + ), + ( + r#""#, + "cannot contain child", + ), + ( + r#""#, + "only valid on ", + ), + ( + r#""#, + "root must be ", + ), + ( + r#""#, + "CDATA is not supported", + ), + ]; + + for (source, expected) in cases { + let error: n0_xml::ParseError = n0_xml::parse(source).unwrap_err(); + assert!( + error.to_string().contains(expected), + "expected `{expected}` in `{error}`" + ); + } +} + +/// Printing cannot omit or invent model state to force a document into the +/// envelope. Non-normalized roots and forests are rejected explicitly. +#[test] +fn print_rejects_documents_outside_the_draft0_root_contract() { + let empty = DocBuilder::new().build(); + assert_eq!( + n0_xml::print(&empty), + Err(PrintError::RenderRootCount { found: 0 }) + ); + + let mut parsed = n0_xml::parse(COMPOSED).unwrap(); + parsed.get_mut(parsed.root).header.width = SizeIntent::Fixed(10.0); + assert_eq!( + n0_xml::print(&parsed), + Err(PrintError::NonCanonicalDocumentRoot) + ); + + let mut shape_root = DocBuilder::new(); + shape_root.add( + 0, + Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + assert_eq!( + n0_xml::print(&shape_root.build()), + Err(PrintError::RenderRootMustBeContainer { found: "shape" }) + ); + + let mut invalid_authored_root = n0_xml::parse(COMPOSED).unwrap(); + let authored_root = invalid_authored_root + .get(invalid_authored_root.root) + .children[0]; + invalid_authored_root.get_mut(authored_root).header.flow = Flow::Absolute; + assert!(matches!( + n0_xml::print(&invalid_authored_root), + Err(PrintError::InvalidDocument(_)) + )); + + let mut derived_span = DocBuilder::new(); + let authored_root = derived_span.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let mut group_header = Header::new(SizeIntent::Auto, SizeIntent::Auto); + group_header.x = AxisBinding::Span { + start: 0.0, + end: 0.0, + }; + derived_span.add(authored_root, group_header, Payload::Group); + assert!(matches!( + n0_xml::print(&derived_span.build()), + Err(PrintError::InvalidDocument(_)) + )); +} + +#[test] +fn print_roundtrip_ignores_arena_ids_but_not_tree_integrity() { + let source = r#" + + + + + + +"#; + let mut tombstoned = n0_xml::parse(source).unwrap(); + let container = tombstoned.get(tombstoned.root).children[0]; + let removed = tombstoned.get(container).children[0]; + let survivor = tombstoned.get(container).children[1]; + tombstoned.remove_subtree(removed); + assert_eq!(tombstoned.get(container).children, vec![survivor]); + let printed = n0_xml::print(&tombstoned).expect("tombstoned ids are storage only"); + assert!(!printed.contains("name=\"A\"")); + assert!(printed.contains("name=\"B\"")); + n0_xml::parse(&printed).expect("printed survivor reparses"); + + let mut sparse = DocBuilder::new(); + let authored_root = sparse.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let mut sparse = sparse.build(); + sparse.add_child( + authored_root, + Node::new( + 9, + Header::new(SizeIntent::Fixed(10.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ), + ); + n0_xml::print(&sparse).expect("non-DFS arena ids are storage only"); + + let canonical = DocBuilder::new().build(); + let mut relocated_root = canonical.get(canonical.root).clone(); + relocated_root.id = 7; + relocated_root.children = vec![9]; + let relocated_container = Node::new( + 9, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let relocated = Document::from_map( + BTreeMap::from([(7, relocated_root), (9, relocated_container)]), + 7, + ); + n0_xml::print(&relocated).expect("root ids are storage only"); + + let mut unreachable = n0_xml::parse(COMPOSED).unwrap(); + let container = unreachable.get(unreachable.root).children[0]; + unreachable.get_mut(container).children.clear(); + assert!(matches!( + n0_xml::print(&unreachable), + Err(PrintError::InvalidDocument(_)) + )); + + let mut dead_edge = n0_xml::parse(COMPOSED).unwrap(); + dead_edge.get_mut(dead_edge.root).children[0] = 99; + assert!(matches!( + n0_xml::print(&dead_edge), + Err(PrintError::InvalidDocument(_)) + )); + + let mut dead_root = DocBuilder::new().build(); + dead_root.remove_subtree(dead_root.root); + assert!(matches!( + n0_xml::print(&dead_root), + Err(PrintError::InvalidDocument(_)) + )); +} diff --git a/crates/n0-model/tests/op_apply.rs b/crates/n0-model/tests/op_apply.rs new file mode 100644 index 00000000..8ad0a288 --- /dev/null +++ b/crates/n0-model/tests/op_apply.rs @@ -0,0 +1,137 @@ +//! ENG-5.1 · the typed `Op` + `apply` dispatcher must never diverge from +//! the free functions they wrap — every variant applied both ways from the +//! same start must leave the same result AND the same document. This is the +//! guard that lets the journal/replay describe writes by `Op` alone. + +mod common; +use common::{opts_visual, shape}; + +use n0_model::model::*; +use n0_model::ops::{self, Axis, Op, OpError, ResizeDrag}; +use n0_model::resolve::{resolve, Resolved}; + +/// root (viewport frame) with a free shape `s` and a group `g` of two shapes. +/// Deterministic ids: s = 1, g = 2. +fn fresh() -> (Document, NodeId, NodeId) { + let mut b = DocBuilder::new(); + let (mut sh, sp) = shape(80.0, 40.0); + sh.x = AxisBinding::start(20.0); + sh.y = AxisBinding::start(20.0); + let s = b.add(0, sh, sp); + + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(200.0); + gh.y = AxisBinding::start(60.0); + let g = b.add(0, gh, Payload::Group); + let (c1h, c1p) = shape(40.0, 40.0); + b.add(g, c1h, c1p); + let (mut c2h, c2p) = shape(40.0, 40.0); + c2h.x = AxisBinding::start(56.0); + b.add(g, c2h, c2p); + + (b.build(), s, g) +} + +/// Apply `op` and the equivalent free call from the SAME start + SAME resolve; +/// assert identical result and identical resulting document. +fn check(label: &str, op: Op, free: impl Fn(&mut Document, &Resolved) -> ops::OpResult) { + let (base, _, _) = fresh(); + let r = resolve(&base, &opts_visual()); + + let mut d_apply = base.clone(); + let res_apply = ops::apply(&mut d_apply, &r, &op); + + let mut d_free = base.clone(); + let res_free = free(&mut d_free, &r); + + assert_eq!(res_apply, res_free, "{label}: result mismatch"); + assert!(d_apply == d_free, "{label}: document mismatch"); +} + +#[test] +fn apply_matches_free_fns() { + let (base, s, g) = fresh(); + let r = resolve(&base, &opts_visual()); + + check("set_x", Op::SetX { id: s, value: 44.0 }, |d, rr| { + ops::set_x(d, rr, s, 44.0) + }); + check("set_y", Op::SetY { id: s, value: 33.0 }, |d, rr| { + ops::set_y(d, rr, s, 33.0) + }); + check( + "move_by", + Op::MoveBy { + id: s, + dx: 10.0, + dy: -7.0, + }, + |d, rr| ops::move_by(d, rr, s, 10.0, -7.0), + ); + check( + "set_width", + Op::SetWidth { + id: s, + value: 111.0, + }, + |d, _| ops::set_width(d, s, 111.0), + ); + check( + "set_height", + Op::SetHeight { id: s, value: 22.0 }, + |d, _| ops::set_height(d, s, 22.0), + ); + check( + "set_rotation", + Op::SetRotation { id: s, deg: 17.0 }, + |d, _| ops::set_rotation(d, s, 17.0), + ); + check( + "rotate_derived_center_feel", + Op::RotateDerivedCenterFeel { id: g, deg: 25.0 }, + |d, rr| ops::rotate_derived_center_feel(d, rr, g, 25.0), + ); + check( + "resize_top_left", + Op::ResizeTopLeft { + id: s, + x: 5.0, + y: 5.0, + w: 60.0, + h: 30.0, + }, + |d, rr| ops::resize_top_left(d, rr, s, 5.0, 5.0, 60.0, 30.0), + ); + check("ungroup", Op::Ungroup { id: g }, |d, rr| { + ops::ungroup(d, rr, g) + }); + check("delete", Op::Delete { id: s }, |d, _| ops::delete(d, s)); + + // resize_drag past the fixed (right) edge — the cross-zero flip path. + let drag = ResizeDrag::begin(&base, &r, s, Axis::X, AnchorEdge::End).unwrap(); + let b = r.box_of(s); + let target = b.x + b.w + 30.0; + check( + "resize_drag", + Op::ResizeDrag { + id: s, + drag, + target, + }, + move |d, rr| ops::resize_drag(d, rr, s, &drag, target), + ); +} + +/// A rejected op must surface the SAME typed error both ways and leave the +/// document byte-untouched (M-6) — errors are deterministic no-ops. +#[test] +fn apply_surfaces_errors_like_free_fns() { + let (base, _s, g) = fresh(); + let r = resolve(&base, &opts_visual()); + + // Sizing a derived (group) box: BoxDerived, doc untouched. + let mut d_apply = base.clone(); + let res_apply = ops::apply(&mut d_apply, &r, &Op::SetWidth { id: g, value: 50.0 }); + assert_eq!(res_apply, Err(OpError::BoxDerived)); + assert!(d_apply == base, "rejected op must not mutate the document"); +} diff --git a/crates/n0-model/tests/ops_suite.rs b/crates/n0-model/tests/ops_suite.rs new file mode 100644 index 00000000..8452292a --- /dev/null +++ b/crates/n0-model/tests/ops_suite.rs @@ -0,0 +1,180 @@ +//! M-* / editor.md doctrine — typed errors, write counts, M-6 atomicity. + +mod common; +use common::*; + +use n0_model::model::*; +use n0_model::ops::{self, OpError}; + +fn free_shape_doc() -> (Document, NodeId) { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::start(10.0); + h.y = AxisBinding::start(20.0); + let s = b.add(0, h, p); + (b.build(), s) +} + +/// editor.md write-count doctrine: the gesture table is enforced. +#[test] +fn write_counts_match_doctrine() { + // rotate (boxed) = 1 + let (mut doc, s) = free_shape_doc(); + assert_eq!(ops::set_rotation(&mut doc, s, 30.0).unwrap(), 1); + + // move = 2 + let (mut doc, s) = free_shape_doc(); + let r = run(&doc); + assert_eq!(ops::move_by(&mut doc, &r, s, 5.0, 5.0).unwrap(), 2); + + // corner resize = 4 + let (mut doc, s) = free_shape_doc(); + let r = run(&doc); + assert_eq!( + ops::resize_top_left(&mut doc, &r, s, 0.0, 0.0, 150.0, 100.0).unwrap(), + 4 + ); + + // group center-feel rotate = 3 (asserted in rotation.rs too) +} + +/// Writes re-target intent: setting x on an End-pinned axis rewrites the +/// end offset so the resolved x equals the given value — the binding kind +/// (the user's intent) is preserved. +#[test] +fn set_x_retargets_end_pin() { + let mut b = DocBuilder::new(); + let (mut fh, fp) = frame_free(SizeIntent::Fixed(400.0), SizeIntent::Fixed(300.0)); + fh.x = AxisBinding::start(0.0); + let f = b.add(0, fh, fp); + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::end(24.0); // resolved x = 256 + let s = b.add(f, h, p); + let mut doc = b.build(); + let r = run(&doc); + + assert_eq!(ops::set_x(&mut doc, &r, s, 200.0).unwrap(), 1); + // intent preserved: + assert!(matches!( + doc.get(s).header.x, + AxisBinding::Pin { + anchor: AnchorEdge::End, + .. + } + )); + // and effective: + let r2 = run(&doc); + assert_close(r2.box_of(s).x, 200.0, "resolved x = requested"); + // offset now 400 − 200 − 120 = 80 + if let AxisBinding::Pin { offset, .. } = doc.get(s).header.x { + assert_close(offset, 80.0, "end offset rewritten"); + } +} + +/// Typed error: G-3 over-constraint is unrepresentable; the write that +/// would create it is rejected as AxisOwnedBySpan. +#[test] +fn set_width_on_spanned_axis_typed_error() { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::Span { + start: 10.0, + end: 20.0, + }; + let s = b.add(0, h, p); + let mut doc = b.build(); + let before = doc.clone(); + assert_eq!( + ops::set_width(&mut doc, s, 300.0), + Err(OpError::AxisOwnedBySpan) + ); + assert_eq!(doc, before, "M-6: rejected write leaves document identical"); +} + +/// Typed error: x-write on an in-flow child under flex (§6). +#[test] +fn set_x_under_flow_typed_error() { + let mut b = DocBuilder::new(); + let (h, p) = frame_flex( + SizeIntent::Fixed(300.0), + SizeIntent::Fixed(100.0), + Direction::Row, + 0.0, + 0.0, + ); + let f = b.add(0, h, p); + let (h1, p1) = shape(50.0, 50.0); + let a = b.add(f, h1, p1); + let mut doc = b.build(); + let r = run(&doc); + let before = doc.clone(); + assert_eq!( + ops::set_x(&mut doc, &r, a, 99.0), + Err(OpError::OwnedByLayout) + ); + assert_eq!(doc, before, "M-6"); +} + +/// R-E3 / N-2: NaN/Inf rejected at the write boundary with a typed error; +/// the document stays NaN-free by construction. +#[test] +fn nan_rejected_at_write_boundary() { + let (mut doc, s) = free_shape_doc(); + let before = doc.clone(); + assert_eq!( + ops::set_rotation(&mut doc, s, f32::NAN), + Err(OpError::InvalidNumber) + ); + assert_eq!( + ops::set_width(&mut doc, s, f32::INFINITY), + Err(OpError::InvalidNumber) + ); + assert_eq!(doc, before, "M-6"); +} + +/// Typed error: size writes on derived-box kinds. +#[test] +fn set_width_on_group_typed_error() { + let mut b = DocBuilder::new(); + let g = b.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let (h1, p1) = shape(40.0, 40.0); + b.add(g, h1, p1); + let mut doc = b.build(); + assert_eq!(ops::set_width(&mut doc, g, 100.0), Err(OpError::BoxDerived)); +} + +/// C-1/C-2 shape (executable H3): move ∥ rotate and resize ∥ rotate touch +/// disjoint scalar fields — a field-level merge preserves both intents. +#[test] +fn concurrency_atoms_disjoint_fields() { + let (doc, s) = free_shape_doc(); + + // replica A: move + let mut da = doc.clone(); + let ra = run(&da); + ops::move_by(&mut da, &ra, s, 30.0, 0.0).unwrap(); + + // replica B: rotate + let mut db = doc.clone(); + ops::set_rotation(&mut db, s, 45.0).unwrap(); + + // field-level merge (x/y from A, rotation from B) + let mut merged = doc.clone(); + merged.get_mut(s).header.x = da.get(s).header.x; + merged.get_mut(s).header.y = da.get(s).header.y; + merged.get_mut(s).header.rotation = db.get(s).header.rotation; + + let rm = run(&merged); + let b = rm.box_of(s); + assert_close(b.x, 40.0, "move survived"); + assert_eq!(merged.get(s).header.rotation, 45.0, "rotation survived"); + // merged document is valid — resolution succeeds with no error reports + assert!(rm + .reports + .iter() + .all(|r| !matches!(r, n0_model::resolve::Report::ErrorByRule { .. }))); +} diff --git a/crates/n0-model/tests/path_geometry_properties.rs b/crates/n0-model/tests/path_geometry_properties.rs new file mode 100644 index 00000000..08b1f081 --- /dev/null +++ b/crates/n0-model/tests/path_geometry_properties.rs @@ -0,0 +1,308 @@ +//! Format-neutral effective path geometry through the ordinary value view. + +use std::sync::Arc; + +use n0_model::math::RectF; +use n0_model::model::{Document, FillRule, NodeId, Payload, ShapeDesc, StrokeAlign}; +use n0_model::n0_xml; +use n0_model::path::{self, PathCommand, PathGeometry}; +use n0_model::properties::{ + PropertyError, PropertyKey, PropertyTarget, PropertyValue, PropertyValues, ValueView, +}; +use n0_model::resolve::{resolve, resolve_view, ResolveOptions}; + +fn only_child(document: &Document) -> NodeId { + let scene = document.get(document.root).children[0]; + document.get(scene).children[0] +} + +fn target(document: &Document, node: NodeId) -> PropertyTarget { + PropertyTarget::new( + document.key_of(node).expect("live node"), + PropertyKey::PathGeometry, + ) +} + +#[test] +fn checked_command_factory_derives_bounds_and_contour_closure() { + let geometry = PathGeometry::from_commands(vec![ + PathCommand::MoveTo { x: 0.25, y: 0.25 }, + PathCommand::LineTo { x: 0.75, y: 0.25 }, + PathCommand::LineTo { x: 0.5, y: 0.75 }, + PathCommand::Close, + ]) + .unwrap(); + + assert_eq!( + geometry.unit_bounds, + RectF { + x: 0.25, + y: 0.25, + w: 0.5, + h: 0.5, + } + ); + assert!(geometry.all_contours_closed); + geometry.validate().unwrap(); + + let non_finite = PathGeometry::from_commands(vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::LineTo { + x: f32::NAN, + y: 1.0, + }, + ]) + .unwrap_err(); + assert!(non_finite.to_string().contains("finite")); + + let malformed = + PathGeometry::from_commands(vec![PathCommand::LineTo { x: 1.0, y: 1.0 }]).unwrap_err(); + assert!(malformed.to_string().contains("follow a move")); +} + +#[test] +fn svg_reference_box_normalizes_after_arc_lowering() { + let source = "M64 32 A32 16 0 0 1 128 32 Z"; + let artifact = path::analyze_in_reference_box(source, FillRule::NonZero, 256.0, 128.0).unwrap(); + assert_eq!(artifact.d(), source); + assert_eq!(artifact.source_reference_box(), (256.0, 128.0)); + assert_eq!( + artifact.geometry().commands[0], + PathCommand::MoveTo { x: 0.25, y: 0.25 } + ); + assert!(artifact + .geometry() + .commands + .iter() + .any(|command| matches!(command, PathCommand::ConicTo { .. }))); + let resolved = + path::materialize(artifact.geometry(), artifact.fill_rule(), 256.0, 128.0).unwrap(); + assert_eq!( + resolved.commands[0], + PathCommand::MoveTo { x: 64.0, y: 32.0 } + ); + assert!(matches!(resolved.commands.last(), Some(PathCommand::Close))); + + let outside = path::analyze_in_reference_box("M0 0 L257 128", FillRule::NonZero, 256.0, 128.0) + .unwrap_err(); + assert!(outside.to_string().contains("unit reference box")); +} + +#[test] +fn n0_xml_writer_rejects_foreign_reference_box_source() { + let mut document = n0_xml::parse( + r#""#, + ) + .unwrap(); + let node = only_child(&document); + let imported = + path::analyze_in_reference_box("M0 0 L10 10", FillRule::NonZero, 10.0, 10.0).unwrap(); + let Payload::Shape { + desc: ShapeDesc::Path(path), + } = &mut document.get_mut(node).payload + else { + panic!("fixture contains a path") + }; + *path = imported; + + let error = n0_xml::print(&document).unwrap_err().to_string(); + assert!(error.contains("unit reference box"), "{error}"); +} + +#[test] +fn effective_path_replaces_only_geometry_and_materializes_once_through_the_final_box() { + let document = n0_xml::parse( + r#""#, + ) + .unwrap(); + let path = only_child(&document); + let Payload::Shape { + desc: ShapeDesc::Path(_), + } = &document.get(path).payload + else { + panic!("expected path") + }; + let sampled = path::analyze("M .25 .25 L .75 .25 L .5 .75 Z", FillRule::NonZero) + .unwrap() + .geometry() + .clone(); + let values = PropertyValues::new( + &document, + [( + target(&document, path), + PropertyValue::PathGeometry(Arc::clone(&sampled)), + )], + ) + .unwrap(); + let view = ValueView::new(&document, &values).unwrap(); + + assert!(Arc::ptr_eq(view.path_geometry(path), &sampled)); + let resolved = resolve_view(&view, &ResolveOptions::default()); + assert_eq!( + resolved.aabb_of(path), + RectF { + x: 60.0, + y: 45.0, + w: 100.0, + h: 50.0, + } + ); + assert_eq!( + resolved.resolved_path_of(path).fill_rule, + FillRule::EvenOdd, + "effective geometry must not replace the authored fill rule" + ); + + let base = resolve(&document, &ResolveOptions::default()); + let empty = PropertyValues::default(); + let empty_view = ValueView::new(&document, &empty).unwrap(); + let empty_resolved = resolve_view(&empty_view, &ResolveOptions::default()); + assert_eq!(empty_resolved, base); +} + +#[test] +fn effective_path_geometry_rejects_wrong_kind_wrong_arena_and_forged_artifacts() { + let path_document = n0_xml::parse( + r#""#, + ) + .unwrap(); + let path = only_child(&path_document); + let valid = path::analyze("M0 0 L1 1", FillRule::NonZero) + .unwrap() + .geometry() + .clone(); + + let rect_document = n0_xml::parse( + r#""#, + ) + .unwrap(); + let rect = only_child(&rect_document); + let inapplicable = PropertyValues::new( + &rect_document, + [( + target(&rect_document, rect), + PropertyValue::PathGeometry(Arc::clone(&valid)), + )], + ) + .unwrap_err(); + assert!(matches!(inapplicable, PropertyError::Inapplicable { .. })); + + let clone = path_document.clone(); + let wrong_arena = PropertyValues::new( + &clone, + [( + target(&path_document, path), + PropertyValue::PathGeometry(Arc::clone(&valid)), + )], + ) + .unwrap_err(); + assert!(matches!(wrong_arena, PropertyError::StaleTarget { .. })); + + let mut forged = (*valid).clone(); + forged.commands = vec![ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::LineTo { + x: f32::INFINITY, + y: 1.0, + }, + ] + .into(); + let invalid = PropertyValues::new( + &path_document, + [( + target(&path_document, path), + PropertyValue::PathGeometry(Arc::new(forged)), + )], + ) + .unwrap_err(); + assert!(matches!(invalid, PropertyError::InvalidValue { .. })); +} + +#[test] +fn effective_open_path_cannot_invalidate_an_authored_outside_stroke() { + let document = n0_xml::parse( + r##""##, + ) + .unwrap(); + let node = only_child(&document); + let open = path::analyze("M0 0 L1 1", FillRule::NonZero) + .unwrap() + .geometry() + .clone(); + let error = PropertyValues::new( + &document, + [(target(&document, node), PropertyValue::PathGeometry(open))], + ) + .unwrap_err(); + + assert!( + matches!(&error, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("every drawable contour")), + "{error}" + ); +} + +#[test] +fn atomic_path_and_stroke_updates_validate_the_complete_effective_state() { + let document = n0_xml::parse( + r##""##, + ) + .unwrap(); + let node = only_child(&document); + let node_key = document.key_of(node).unwrap(); + let closed = path::analyze("M0 0 L1 0 L1 1 Z", FillRule::NonZero) + .unwrap() + .geometry() + .clone(); + let mut outside_strokes = document.get(node).strokes.clone(); + outside_strokes[0].align = StrokeAlign::Outside; + + for reverse in [false, true] { + let mut entries = vec![ + ( + PropertyTarget::new(node_key, PropertyKey::PathGeometry), + PropertyValue::PathGeometry(Arc::clone(&closed)), + ), + ( + PropertyTarget::new(node_key, PropertyKey::Strokes), + PropertyValue::Strokes(outside_strokes.clone()), + ), + ]; + if reverse { + entries.reverse(); + } + PropertyValues::new(&document, entries).unwrap_or_else(|error| { + panic!("combined state must be valid in either order: {error}") + }); + } +} + +#[test] +fn retained_path_spelling_stays_outside_effective_geometry() { + let document = n0_xml::parse( + r#""#, + ) + .unwrap(); + let node = only_child(&document); + let absolute = path::analyze("M .25 .25 L .75 .25 L .5 .75 Z", FillRule::NonZero).unwrap(); + let relative = path::analyze("m .25 .25 l .5 0 l -.25 .5 z", FillRule::NonZero).unwrap(); + assert_ne!(absolute.d(), relative.d()); + assert!(absolute.same_visual_geometry(&relative)); + + let absolute_values = PropertyValues::new( + &document, + [( + target(&document, node), + PropertyValue::PathGeometry(absolute.geometry().clone()), + )], + ) + .unwrap(); + let relative_values = PropertyValues::new( + &document, + [( + target(&document, node), + PropertyValue::PathGeometry(relative.geometry().clone()), + )], + ) + .unwrap(); + assert_eq!(absolute_values, relative_values); +} diff --git a/crates/n0-model/tests/properties.rs b/crates/n0-model/tests/properties.rs new file mode 100644 index 00000000..873935a9 --- /dev/null +++ b/crates/n0-model/tests/properties.rs @@ -0,0 +1,1037 @@ +//! Closed typed-property registry and immutable ValueView contract. + +use n0_model::model::*; +use n0_model::n0_xml; +use n0_model::pick::pick; +use n0_model::properties::*; +use n0_model::resolve::{resolve, resolve_view, ResolveOptions}; +use std::collections::BTreeSet; + +fn target(document: &Document, node: NodeId, property: PropertyKey) -> PropertyTarget { + PropertyTarget::new(document.key_of(node).unwrap(), property) +} + +fn flex_scene() -> (Document, NodeId, NodeId) { + let source = r##" + + + + + + +"##; + let document = n0_xml::parse(source).unwrap(); + let scene = document.get(document.root).children[0]; + let first = document.get(scene).children[0]; + let second = document.get(scene).children[1]; + (document, first, second) +} + +fn per_side_stroke(payload: &Payload) -> Stroke { + let mut stroke = Stroke::default_for(payload).unwrap(); + stroke.width = StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 1.0, + stroke_right_width: 2.0, + stroke_bottom_width: 3.0, + stroke_left_width: 4.0, + }); + stroke +} + +fn stop(offset: f32) -> GradientStop { + GradientStop { + offset, + color: Color::BLACK, + } +} + +#[test] +fn registry_is_closed_unique_and_every_base_accessor_matches_its_value_kind() { + assert_eq!(property_registry().len(), PropertyKey::ALL.len()); + assert_eq!( + property_registry() + .iter() + .map(|spec| spec.key) + .collect::>() + .len(), + PropertyKey::ALL.len() + ); + + let document = n0_xml::parse( + r#" + + + + + + + text + + + + +"#, + ) + .unwrap(); + let frame = document.get(document.root).children[0]; + let children = &document.get(frame).children; + assert_eq!(children.len(), 7); + + const FRAME_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::AspectRatio, + PropertyKey::LensOps, + PropertyKey::PathGeometry, + ]; + const NON_FRAME: &[PropertyKey] = &[ + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::LensOps, + PropertyKey::PathGeometry, + ]; + const ELLIPSE_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::CornerRadius, + PropertyKey::CornerSmoothing, + PropertyKey::LensOps, + PropertyKey::PathGeometry, + ]; + const PATH_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::CornerRadius, + PropertyKey::CornerSmoothing, + PropertyKey::LensOps, + ]; + const LINE_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::Height, + PropertyKey::MinHeight, + PropertyKey::MaxHeight, + PropertyKey::AspectRatio, + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::CornerRadius, + PropertyKey::CornerSmoothing, + PropertyKey::Fills, + PropertyKey::LensOps, + PropertyKey::PathGeometry, + ]; + const TEXT_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::AspectRatio, + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::CornerRadius, + PropertyKey::CornerSmoothing, + PropertyKey::LensOps, + PropertyKey::PathGeometry, + ]; + const DERIVED_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::Width, + PropertyKey::Height, + PropertyKey::MinWidth, + PropertyKey::MaxWidth, + PropertyKey::MinHeight, + PropertyKey::MaxHeight, + PropertyKey::AspectRatio, + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::CornerRadius, + PropertyKey::CornerSmoothing, + PropertyKey::Fills, + PropertyKey::Strokes, + PropertyKey::PathGeometry, + ]; + const GROUP_INAPPLICABLE: &[PropertyKey] = &[ + PropertyKey::Width, + PropertyKey::Height, + PropertyKey::MinWidth, + PropertyKey::MaxWidth, + PropertyKey::MinHeight, + PropertyKey::MaxHeight, + PropertyKey::AspectRatio, + PropertyKey::Layout, + PropertyKey::ClipsContent, + PropertyKey::CornerRadius, + PropertyKey::CornerSmoothing, + PropertyKey::Fills, + PropertyKey::Strokes, + PropertyKey::LensOps, + PropertyKey::PathGeometry, + ]; + let cases: [(&str, NodeId, &[PropertyKey]); 8] = [ + ("frame", frame, FRAME_INAPPLICABLE), + ("rect", children[0], NON_FRAME), + ("ellipse", children[1], ELLIPSE_INAPPLICABLE), + ("line", children[2], LINE_INAPPLICABLE), + ("path", children[3], PATH_INAPPLICABLE), + ("text", children[4], TEXT_INAPPLICABLE), + ("group", children[5], GROUP_INAPPLICABLE), + ("lens", children[6], DERIVED_INAPPLICABLE), + ]; + + for (kind, id, inapplicable) in cases { + let node = document.get(id); + for spec in property_registry() { + let expected = !inapplicable.contains(&spec.key); + assert_eq!( + spec.applies_to(node), + expected, + "{kind} {:?} applicability", + spec.key + ); + match spec.base_value(node) { + Some(value) => { + assert!(expected); + assert_eq!(value.kind(), spec.value_kind, "{kind} {:?}", spec.key); + } + None => assert!(!expected, "{kind} {:?} base value", spec.key), + } + } + } + + assert!(!PropertyKey::Fills + .spec() + .applies_to(document.get(children[2]))); + assert!(PropertyKey::Strokes + .spec() + .applies_to(document.get(children[2]))); +} + +#[test] +fn property_values_reject_duplicates_stale_wrong_inapplicable_and_invalid_entries() { + let (mut document, first, _) = flex_scene(); + let width = target(&document, first, PropertyKey::Width); + let duplicate = PropertyValues::new( + &document, + [ + (width, PropertyValue::SizeIntent(SizeIntent::Fixed(30.0))), + (width, PropertyValue::SizeIntent(SizeIntent::Fixed(40.0))), + ], + ) + .unwrap_err(); + assert!(matches!(duplicate, PropertyError::DuplicateTarget { .. })); + + let wrong = PropertyValues::new(&document, [(width, PropertyValue::Number(30.0))]).unwrap_err(); + assert!(matches!(wrong, PropertyError::WrongValueKind { .. })); + + let group = { + let mut builder = DocBuilder::new(); + let group = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + (builder.build(), group) + }; + let inapplicable = PropertyValues::new( + &group.0, + [( + target(&group.0, group.1, PropertyKey::Width), + PropertyValue::SizeIntent(SizeIntent::Fixed(1.0)), + )], + ) + .unwrap_err(); + assert!(matches!(inapplicable, PropertyError::Inapplicable { .. })); + + let invalid = PropertyValues::new( + &document, + [( + target(&document, first, PropertyKey::Opacity), + PropertyValue::Number(f32::NAN), + )], + ) + .unwrap_err(); + assert!(matches!(invalid, PropertyError::InvalidValue { .. })); + + let other = document.clone(); + let cross_document = PropertyValues::new( + &other, + [(width, PropertyValue::SizeIntent(SizeIntent::Fixed(30.0)))], + ) + .unwrap_err(); + assert!(matches!(cross_document, PropertyError::StaleTarget { .. })); + + let retained = PropertyValues::new( + &document, + [(width, PropertyValue::SizeIntent(SizeIntent::Fixed(30.0)))], + ) + .unwrap(); + document.remove_subtree(first); + let stale = ValueView::new(&document, &retained).unwrap_err(); + assert!(matches!(stale, PropertyError::StaleTarget { .. })); +} + +#[test] +fn paint_and_stroke_values_are_validated_recursively_without_subobject_targets() { + let (document, first, _) = flex_scene(); + let mut gradient = LinearGradientPaint::default(); + gradient.opacity = f32::INFINITY; + let paint_error = PropertyValues::new( + &document, + [( + target(&document, first, PropertyKey::Fills), + PropertyValue::Paints(Paints::new([Paint::LinearGradient(gradient)])), + )], + ) + .unwrap_err(); + assert!(matches!(paint_error, PropertyError::InvalidValue { .. })); + + let mut stroke = Stroke::default_for(&document.get(first).payload).unwrap(); + stroke.width = StrokeWidth::Uniform(-1.0); + let stroke_error = PropertyValues::new( + &document, + [( + target(&document, first, PropertyKey::Strokes), + PropertyValue::Strokes(vec![stroke]), + )], + ) + .unwrap_err(); + assert!(matches!(stroke_error, PropertyError::InvalidValue { .. })); +} + +#[test] +fn effective_gradients_reject_singular_transforms_for_every_gradient_kind() { + let (document, rect, _) = flex_scene(); + let stops = vec![stop(0.0), stop(1.0)]; + let singular = n0_model::math::Affine { + a: 1.0, + b: 2.0, + c: 2.0, + d: 4.0, + e: 8.0, + f: 9.0, + }; + let paints = [ + Paint::LinearGradient(LinearGradientPaint { + transform: singular, + stops: stops.clone(), + ..Default::default() + }), + Paint::RadialGradient(RadialGradientPaint { + transform: singular, + stops: stops.clone(), + ..Default::default() + }), + Paint::SweepGradient(SweepGradientPaint { + transform: singular, + stops: stops.clone(), + ..Default::default() + }), + Paint::DiamondGradient(DiamondGradientPaint { + transform: singular, + stops, + ..Default::default() + }), + ]; + + for paint in paints { + let error = PropertyValues::new( + &document, + [( + target(&document, rect, PropertyKey::Fills), + PropertyValue::Paints(Paints::new([paint])), + )], + ) + .unwrap_err(); + assert!( + matches!(&error, PropertyError::InvalidValue { reason, .. } if reason.contains("transform must be invertible")), + "{error}" + ); + } +} + +#[test] +fn effective_linear_gradient_endpoints_follow_skias_f32_degeneracy_boundary() { + let (document, rect, _) = flex_scene(); + let key = target(&document, rect, PropertyKey::Fills); + let threshold = n0_model::renderability::LINEAR_GRADIENT_DEGENERATE_THRESHOLD; + let gradient = |distance: f32| { + Paint::LinearGradient(LinearGradientPaint { + xy1: Alignment::from_uv(0.0, 0.5), + xy2: Alignment::from_uv(distance, 0.5), + stops: vec![stop(0.0), stop(1.0)], + ..Default::default() + }) + }; + + for distance in [0.0, threshold / 2.0, threshold] { + let error = PropertyValues::new( + &document, + [( + key, + PropertyValue::Paints(Paints::new([gradient(distance)])), + )], + ) + .unwrap_err(); + assert!( + matches!(&error, PropertyError::InvalidValue { reason, .. } if reason.contains("farther apart")), + "distance={distance}: {error}" + ); + } + + PropertyValues::new( + &document, + [( + key, + PropertyValue::Paints(Paints::new([gradient(threshold * 2.0)])), + )], + ) + .expect("a linear ramp above Skia's degeneracy threshold remains renderable"); +} + +#[test] +fn line_locked_height_and_gradient_stop_cardinality_are_rejected() { + let document = + n0_xml::parse(r#""#) + .unwrap(); + let scene = document.get(document.root).children[0]; + let line = document.get(scene).children[0]; + let height = PropertyValues::new( + &document, + [( + target(&document, line, PropertyKey::Height), + PropertyValue::SizeIntent(SizeIntent::Fixed(1.0)), + )], + ) + .unwrap_err(); + assert!(matches!(height, PropertyError::Inapplicable { .. })); + + let (document, rect, _) = flex_scene(); + for stops in [vec![], vec![stop(0.0)]] { + let gradient = LinearGradientPaint { + stops, + ..Default::default() + }; + let error = PropertyValues::new( + &document, + [( + target(&document, rect, PropertyKey::Fills), + PropertyValue::Paints(Paints::new([Paint::LinearGradient(gradient)])), + )], + ) + .unwrap_err(); + assert!( + matches!(&error, PropertyError::InvalidValue { reason, .. } if reason.contains("at least two stops")), + "{error}" + ); + } +} + +#[test] +fn base_and_empty_effective_views_both_reject_inapplicable_reads() { + let document = + n0_xml::parse(r#""#) + .unwrap(); + let scene = document.get(document.root).children[0]; + let line = document.get(scene).children[0]; + let empty = PropertyValues::default(); + let base = ValueView::base(&document); + let effective = ValueView::new(&document, &empty).unwrap(); + + assert!(std::panic::catch_unwind(|| base.height(line)).is_err()); + assert!(std::panic::catch_unwind(|| effective.height(line)).is_err()); +} + +#[test] +fn unrelated_effective_values_do_not_make_flex_read_inapplicable_geometry() { + let mut builder = DocBuilder::new(); + let frame = builder.add( + 0, + Header::new(SizeIntent::Fixed(200.0), SizeIntent::Fixed(80.0)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + direction: Direction::Row, + ..Default::default() + }, + clips_content: false, + }, + ); + let line = builder.add( + frame, + Header::new(SizeIntent::Fixed(20.0), SizeIntent::Auto), + Payload::Shape { + desc: ShapeDesc::Line, + }, + ); + let group = builder.add( + frame, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + builder.add( + group, + Header::new(SizeIntent::Fixed(12.0), SizeIntent::Fixed(10.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let lens = builder.add( + frame, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { ops: vec![] }, + ); + builder.add( + lens, + Header::new(SizeIntent::Fixed(8.0), SizeIntent::Fixed(6.0)), + Payload::Shape { + desc: ShapeDesc::Rect, + }, + ); + let document = builder.build(); + + for node in [line, group, lens] { + let values = PropertyValues::new( + &document, + [( + target(&document, node, PropertyKey::Opacity), + PropertyValue::Number(0.5), + )], + ) + .unwrap(); + let view = ValueView::new(&document, &values).unwrap(); + let resolved = resolve_view(&view, &ResolveOptions::default()); + assert!(resolved.box_opt(node).is_some()); + } +} + +#[test] +fn effective_geometry_tuples_preserve_static_span_line_and_shape_rules() { + let (document, rect, _) = flex_scene(); + let span_with_fixed = PropertyValues::new( + &document, + [( + target(&document, rect, PropertyKey::X), + PropertyValue::AxisBinding(AxisBinding::Span { + start: 0.0, + end: 0.0, + }), + )], + ) + .unwrap_err(); + assert!( + matches!(&span_with_fixed, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("span x")), + "{span_with_fixed}" + ); + + let constrained = n0_xml::parse( + r#""#, + ) + .unwrap(); + let scene = constrained.get(constrained.root).children[0]; + let rect = constrained.get(scene).children[0]; + let span_with_min = PropertyValues::new( + &constrained, + [( + target(&constrained, rect, PropertyKey::MinWidth), + PropertyValue::OptionalNumber(Some(1.0)), + )], + ) + .unwrap_err(); + assert!(matches!( + span_with_min, + PropertyError::InvalidEffectiveState { .. } + )); + + let line_document = + n0_xml::parse(r#""#) + .unwrap(); + let scene = line_document.get(line_document.root).children[0]; + let line = line_document.get(scene).children[0]; + let line_y = PropertyValues::new( + &line_document, + [( + target(&line_document, line, PropertyKey::Y), + PropertyValue::AxisBinding(AxisBinding::Span { + start: 0.0, + end: 0.0, + }), + )], + ) + .unwrap_err(); + assert!( + matches!(&line_y, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("y Span")), + "{line_y}" + ); + let mut bad_line_document = line_document.clone(); + bad_line_document.get_mut(line).header.height = SizeIntent::Fixed(1.0); + let bad_height = PropertyValues::new( + &bad_line_document, + [( + target(&bad_line_document, line, PropertyKey::Width), + PropertyValue::SizeIntent(SizeIntent::Fixed(12.0)), + )], + ) + .unwrap_err(); + assert!( + matches!(&bad_height, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("locked to zero")), + "{bad_height}" + ); + + let mut builder = DocBuilder::new(); + let group = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Group, + ); + let lens = builder.add( + 0, + Header::new(SizeIntent::Auto, SizeIntent::Auto), + Payload::Lens { ops: vec![] }, + ); + let derived = builder.build(); + for node in [group, lens] { + let error = PropertyValues::new( + &derived, + [( + target(&derived, node, PropertyKey::X), + PropertyValue::AxisBinding(AxisBinding::Span { + start: 0.0, + end: 0.0, + }), + )], + ) + .unwrap_err(); + assert!( + matches!(&error, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("derived origin")), + "{error}" + ); + } + + let shapes = n0_xml::parse( + r#""#, + ) + .unwrap(); + let scene = shapes.get(shapes.root).children[0]; + for &shape in &shapes.get(scene).children { + let error = PropertyValues::new( + &shapes, + [( + target(&shapes, shape, PropertyKey::Width), + PropertyValue::SizeIntent(SizeIntent::Auto), + )], + ) + .unwrap_err(); + assert!( + matches!(&error, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("require")), + "{error}" + ); + } +} + +#[test] +fn effective_corner_and_stroke_pairs_are_validated_as_one_state() { + let (mut smoothing_document, rect, _) = flex_scene(); + smoothing_document.get_mut(rect).corner_smoothing = CornerSmoothing(0.5); + let stroke = per_side_stroke(&smoothing_document.get(rect).payload); + let stroke_error = PropertyValues::new( + &smoothing_document, + [( + target(&smoothing_document, rect, PropertyKey::Strokes), + PropertyValue::Strokes(vec![stroke.clone()]), + )], + ) + .unwrap_err(); + assert!( + matches!(&stroke_error, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("corner-smoothing")), + "{stroke_error}" + ); + + let (mut stroke_document, rect, _) = flex_scene(); + stroke_document.get_mut(rect).strokes = vec![stroke.clone()]; + let smoothing_error = PropertyValues::new( + &stroke_document, + [( + target(&stroke_document, rect, PropertyKey::CornerSmoothing), + PropertyValue::Number(0.5), + )], + ) + .unwrap_err(); + assert!( + matches!(&smoothing_error, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("corner-smoothing")), + "{smoothing_error}" + ); + + let (mut elliptical_document, rect, _) = flex_scene(); + elliptical_document.get_mut(rect).corner_radius = + RectangularCornerRadius::all(Radius { rx: 8.0, ry: 4.0 }); + let elliptical_error = PropertyValues::new( + &elliptical_document, + [( + target(&elliptical_document, rect, PropertyKey::CornerSmoothing), + PropertyValue::Number(0.5), + )], + ) + .unwrap_err(); + assert!( + matches!(&elliptical_error, PropertyError::InvalidEffectiveState { reason, .. } if reason.contains("circular corner radii")), + "{elliptical_error}" + ); + + let (mut radius_document, rect, _) = flex_scene(); + radius_document.get_mut(rect).corner_smoothing = CornerSmoothing(0.5); + let radius_error = PropertyValues::new( + &radius_document, + [( + target(&radius_document, rect, PropertyKey::CornerRadius), + PropertyValue::CornerRadius(RectangularCornerRadius::all(Radius { rx: 8.0, ry: 4.0 })), + )], + ) + .unwrap_err(); + assert!(matches!( + radius_error, + PropertyError::InvalidEffectiveState { .. } + )); + + let coordinated = PropertyValues::new( + &smoothing_document, + [ + ( + target(&smoothing_document, rect, PropertyKey::CornerSmoothing), + PropertyValue::Number(0.0), + ), + ( + target(&smoothing_document, rect, PropertyKey::Strokes), + PropertyValue::Strokes(vec![stroke]), + ), + ], + ) + .unwrap(); + ValueView::new(&smoothing_document, &coordinated).unwrap(); +} + +#[test] +fn value_view_revalidates_aggregate_state_after_authored_changes() { + let (mut document, rect, _) = flex_scene(); + let values = PropertyValues::new( + &document, + [( + target(&document, rect, PropertyKey::Strokes), + PropertyValue::Strokes(vec![per_side_stroke(&document.get(rect).payload)]), + )], + ) + .unwrap(); + document.get_mut(rect).corner_smoothing = CornerSmoothing(0.5); + assert!(matches!( + ValueView::new(&document, &values), + Err(PropertyError::InvalidEffectiveState { .. }) + )); +} + +#[test] +fn effective_image_paints_match_the_proving_renderer_capability_fence() { + let (document, rect, _) = flex_scene(); + let key = target(&document, rect, PropertyKey::Fills); + let valid = ImagePaint::from_rid("asset"); + PropertyValues::new( + &document, + [( + key, + PropertyValue::Paints(Paints::new([Paint::Image(valid.clone())])), + )], + ) + .unwrap(); + + let mut unsupported = Vec::new(); + let mut quarter_turn = valid.clone(); + quarter_turn.quarter_turns = 1; + unsupported.push(quarter_turn); + let mut alignment = valid.clone(); + alignment.alignment = Alignment(-1.0, -1.0); + unsupported.push(alignment); + let mut transform = valid.clone(); + transform.fit = ImagePaintFit::Transform(n0_model::math::Affine::IDENTITY); + unsupported.push(transform); + let mut tile = valid.clone(); + tile.fit = ImagePaintFit::Tile(ImageTile { + scale: 1.0, + repeat: ImageRepeat::Repeat, + }); + unsupported.push(tile); + let mut filtered = valid; + filtered.filters.exposure = 1.0; + unsupported.push(filtered); + + for image in unsupported { + let error = PropertyValues::new( + &document, + [( + key, + PropertyValue::Paints(Paints::new([Paint::Image(image)])), + )], + ) + .unwrap_err(); + assert!(matches!(error, PropertyError::InvalidValue { .. })); + } +} + +#[test] +fn absent_values_read_authored_base_while_typed_none_clears_optional_base() { + let (mut document, first, _) = flex_scene(); + document.get_mut(first).header.min_width = Some(12.0); + let key = target(&document, first, PropertyKey::MinWidth); + + let base = ValueView::base(&document); + assert_eq!(base.min_width(first), Some(12.0)); + + let values = + PropertyValues::new(&document, [(key, PropertyValue::OptionalNumber(None))]).unwrap(); + let projected = ValueView::new(&document, &values).unwrap(); + assert_eq!(projected.min_width(first), None); + assert_eq!(document.get(first).header.min_width, Some(12.0)); +} + +#[test] +fn empty_value_view_is_bit_exact_for_resolve_and_pick() { + let source = r##" + + + alpha beta + + + +"##; + let document = n0_xml::parse(source).unwrap(); + let values = PropertyValues::new(&document, []).unwrap(); + let view = ValueView::new(&document, &values).unwrap(); + let options = ResolveOptions::default(); + let base = resolve(&document, &options); + let projected = resolve_view(&view, &options); + assert_eq!(projected, base); + assert_eq!(pick(&projected, 1.0, 1.0), pick(&base, 1.0, 1.0)); +} + +#[test] +fn valid_geometry_transform_and_visibility_values_change_only_the_view() { + let (document, first, second) = flex_scene(); + let authored = document.clone(); + let options = ResolveOptions::default(); + let base = resolve(&document, &options); + + let width_values = PropertyValues::new( + &document, + [( + target(&document, first, PropertyKey::Width), + PropertyValue::SizeIntent(SizeIntent::Fixed(50.0)), + )], + ) + .unwrap(); + let width_view = ValueView::new(&document, &width_values).unwrap(); + let width_resolved = resolve_view(&width_view, &options); + assert_eq!(base.box_of(second).x, 20.0); + assert_eq!(width_resolved.box_of(second).x, 50.0); + + let rotation_values = PropertyValues::new( + &document, + [( + target(&document, first, PropertyKey::Rotation), + PropertyValue::Number(45.0), + )], + ) + .unwrap(); + let rotation_view = ValueView::new(&document, &rotation_values).unwrap(); + let rotation_resolved = resolve_view(&rotation_view, &options); + assert_ne!(rotation_resolved.world_of(first), base.world_of(first)); + assert_ne!(rotation_resolved.aabb_of(first), base.aabb_of(first)); + + let active_values = PropertyValues::new( + &document, + [( + target(&document, first, PropertyKey::Active), + PropertyValue::Boolean(false), + )], + ) + .unwrap(); + let active_view = ValueView::new(&document, &active_values).unwrap(); + let active_resolved = resolve_view(&active_view, &options); + assert!(active_resolved.world_opt(first).is_none()); + assert_eq!(active_resolved.box_of(second).x, 0.0); + + assert_eq!( + document, authored, + "value views never mutate authored state" + ); +} + +#[test] +fn effective_cross_size_controls_container_stretch_without_mutating_authored_state() { + let mut builder = DocBuilder::new(); + let container = builder.add( + 0, + Header::new(SizeIntent::Fixed(100.0), SizeIntent::Fixed(100.0)), + Payload::Frame { + layout: LayoutBehavior { + mode: LayoutMode::Flex, + direction: Direction::Column, + cross_align: CrossAlign::Stretch, + ..Default::default() + }, + clips_content: false, + }, + ); + let authored_auto = builder.add( + container, + Header::new(SizeIntent::Auto, SizeIntent::Fixed(20.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let authored_fixed = builder.add( + container, + Header::new(SizeIntent::Fixed(30.0), SizeIntent::Fixed(20.0)), + Payload::Frame { + layout: LayoutBehavior::default(), + clips_content: false, + }, + ); + let document = builder.build(); + let authored = document.clone(); + let values = PropertyValues::new( + &document, + [ + ( + target(&document, authored_auto, PropertyKey::Width), + PropertyValue::SizeIntent(SizeIntent::Fixed(40.0)), + ), + ( + target(&document, authored_fixed, PropertyKey::Width), + PropertyValue::SizeIntent(SizeIntent::Auto), + ), + ], + ) + .unwrap(); + let view = ValueView::new(&document, &values).unwrap(); + let resolved = resolve_view(&view, &ResolveOptions::default()); + + assert_eq!( + resolved.box_of(authored_auto).w, + 40.0, + "an effective fixed cross size prevents container stretch" + ); + assert_eq!( + resolved.box_of(authored_fixed).w, + 100.0, + "an effective Auto cross size permits container stretch" + ); + assert_eq!( + document, authored, + "resolution leaves authored state intact" + ); +} + +#[test] +fn rounded_and_smoothed_effective_clips_control_descendant_queries() { + let document = n0_xml::parse( + r#" + + + + + + + +"#, + ) + .unwrap(); + let scene = document.get(document.root).children[0]; + let clip = document.get(scene).children[0]; + let child = document.get(clip).children[0]; + let options = ResolveOptions::default(); + + let base = resolve(&document, &options); + assert_eq!(pick(&base, 11.0, 11.0), Some(child)); + + let rounded_values = PropertyValues::new( + &document, + [( + target(&document, clip, PropertyKey::CornerRadius), + PropertyValue::CornerRadius(RectangularCornerRadius::circular(30.0)), + )], + ) + .unwrap(); + let rounded_view = ValueView::new(&document, &rounded_values).unwrap(); + let rounded = resolve_view(&rounded_view, &options); + assert_eq!( + pick(&rounded, 11.0, 11.0), + Some(clip), + "the rounded corner excludes the child but not the frame's own box" + ); + assert_eq!(pick(&rounded, 50.0, 11.0), Some(child)); + assert_eq!(pick(&rounded, 50.0, 50.0), Some(child)); + + let ordinary_values = PropertyValues::new( + &document, + [( + target(&document, clip, PropertyKey::CornerRadius), + PropertyValue::CornerRadius(RectangularCornerRadius::circular(24.0)), + )], + ) + .unwrap(); + let ordinary_view = ValueView::new(&document, &ordinary_values).unwrap(); + let ordinary = resolve_view(&ordinary_view, &options); + assert_eq!(pick(&ordinary, 28.0, 11.0), Some(child)); + + let smooth_values = PropertyValues::new( + &document, + [ + ( + target(&document, clip, PropertyKey::CornerRadius), + PropertyValue::CornerRadius(RectangularCornerRadius::circular(24.0)), + ), + ( + target(&document, clip, PropertyKey::CornerSmoothing), + PropertyValue::Number(0.8), + ), + ], + ) + .unwrap(); + let smooth_view = ValueView::new(&document, &smooth_values).unwrap(); + let smooth = resolve_view(&smooth_view, &options); + assert_eq!( + pick(&smooth, 28.0, 11.0), + Some(clip), + "effective smoothing uses the same extended corner path as raster clipping" + ); +} + +#[test] +fn impacts_cover_the_declared_downstream_stages() { + assert!(PropertyKey::Width + .spec() + .impact + .contains(PropertyImpact::LAYOUT)); + for key in [ + PropertyKey::Rotation, + PropertyKey::FlipX, + PropertyKey::FlipY, + ] { + assert!( + key.spec() + .impact + .contains(PropertyImpact::MEASURE | PropertyImpact::LAYOUT), + "{key:?} participates in AABB-in-flow measurement and layout" + ); + assert!(key + .spec() + .impact + .contains(PropertyImpact::TRANSFORM | PropertyImpact::BOUNDS | PropertyImpact::PAINT)); + } + assert!(PropertyKey::Strokes + .spec() + .impact + .contains(PropertyImpact::BOUNDS | PropertyImpact::PAINT)); + assert!(PropertyKey::PathGeometry + .spec() + .impact + .contains(PropertyImpact::BOUNDS | PropertyImpact::PAINT)); + assert!(!PropertyKey::PathGeometry + .spec() + .impact + .contains(PropertyImpact::LAYOUT)); + assert!(PropertyKey::Fills + .spec() + .impact + .contains(PropertyImpact::RESOURCE)); +} diff --git a/crates/n0-model/tests/rotation.rs b/crates/n0-model/tests/rotation.rs new file mode 100644 index 00000000..fb8a7c51 --- /dev/null +++ b/crates/n0-model/tests/rotation.rs @@ -0,0 +1,211 @@ +//! R-* — rotation & transform semantics, including the E1 FORK row (R-3). + +mod common; +use common::*; + +use n0_model::math::{rotated_aabb_size, Affine}; +use n0_model::model::*; +use n0_model::resolve::resolve; + +/// R-1 (locked POL): rotating a boxed node preserves its box center. +#[test] +fn r1_center_pivot_preserves_center() { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::start(10.0); + h.y = AxisBinding::start(20.0); + let s = b.add(0, h, p); + let mut doc = b.build(); + + let r0 = run(&doc); + let c0 = r0.world_of(s).apply((60.0, 40.0)); + + for deg in [15.0, 90.0, 213.0, -45.0] { + doc.get_mut(s).header.rotation = deg; + let r = run(&doc); + let c = r.world_of(s).apply((60.0, 40.0)); + assert_close(c.0, c0.0, "center x invariant"); + assert_close(c.1, c0.1, "center y invariant"); + } +} + +/// R-2: world AABB is the exact |w·cosθ|+|h·sinθ| envelope. +#[test] +fn r2_world_aabb_is_exact_envelope() { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(120.0, 80.0); + h.rotation = 37.0; + let s = b.add(0, h, p); + let doc = b.build(); + let r = run(&doc); + let aabb = r.aabb_of(s); + let (we, he) = rotated_aabb_size(120.0, 80.0, 37.0); + assert_close(aabb.w, we, "aabb w"); + assert_close(aabb.h, he, "aabb h"); +} + +/// R-4: resize-then-rotate ≡ rotate-then-resize under the center pivot, +/// compared on the box (position + extent), which is what the document +/// stores. (World transforms differ by construction: the pivot moves with +/// the box center.) +#[test] +fn r4_rotation_resize_commute_on_box() { + let build = |order: bool| { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(120.0, 80.0); + h.x = AxisBinding::start(10.0); + h.y = AxisBinding::start(20.0); + let s = b.add(0, h, p); + let mut doc = b.build(); + if order { + doc.get_mut(s).header.rotation = 30.0; + doc.get_mut(s).header.width = SizeIntent::Fixed(200.0); + } else { + doc.get_mut(s).header.width = SizeIntent::Fixed(200.0); + doc.get_mut(s).header.rotation = 30.0; + } + (doc, s) + }; + let (d1, s1) = build(true); + let (d2, s2) = build(false); + assert_eq!(run(&d1).box_of(s1), run(&d2).box_of(s2)); + assert_eq!(run(&d1).world_of(s1), run(&d2).world_of(s2)); +} + +/// R-5: 50-deep rotated nesting matches closed-form composition within N-3. +#[test] +fn r5_deep_nesting_matches_closed_form() { + let mut b = DocBuilder::new(); + let mut parent = 0; + let deg = 3.0f32; + for _ in 0..50 { + let (mut h, p) = frame_free(SizeIntent::Fixed(200.0), SizeIntent::Fixed(200.0)); + h.x = AxisBinding::start(5.0); + h.y = AxisBinding::start(4.0); + h.rotation = deg; + parent = b.add(parent, h, p); + } + let doc = b.build(); + let r = run(&doc); + + // closed form: fold the same local transforms + let mut expect = Affine::IDENTITY; + for _ in 0..50 { + expect = expect.then(&Affine::from_box_center(5.0, 4.0, 200.0, 200.0, deg)); + } + let got = r.world_of(parent); + // 50 compositions: allow a looser (but still tight) bound + for (g, e) in [ + (got.a, expect.a), + (got.b, expect.b), + (got.c, expect.c), + (got.d, expect.d), + (got.e, expect.e), + (got.f, expect.f), + ] { + assert!((g - e).abs() < 1e-2, "closed form: {g} vs {e}"); + } +} + +/// R-3 — THE fork row, both arms, locked per mode (E1 executable form). +/// +/// Portrait cards (60×100) in a row; the middle one rotated 90° so its +/// AABB (100×60) is *wider* than its box — the case where the two +/// semantics visibly diverge. +#[test] +fn r3_rotation_in_flow_fork_both_arms() { + let mut b = DocBuilder::new(); + let f = { + let (h, p) = frame_flex( + SizeIntent::Auto, + SizeIntent::Fixed(140.0), + Direction::Row, + 10.0, + 10.0, + ); + b.add(0, h, p) + }; + let mut cards = vec![]; + for _ in 0..3 { + let (h, p) = shape(60.0, 100.0); + cards.push(b.add(f, h, p)); + } + let mut doc = b.build(); + doc.get_mut(cards[1]).header.rotation = 90.0; + + // anchor arm: middle slot = rotated AABB (100 wide); siblings make room + let ra = resolve(&doc, &opts()); + assert_close(ra.box_of(cards[2]).x, 190.0, "third card after AABB slot"); // 10+60+10+100+10 + assert_close(ra.box_of(f).w, 260.0, "hug = pad+60+gap+100+gap+60+pad"); + // box center := slot center (slot 80..180 → center 130; box 100..160… box w=60) + assert_close(ra.box_of(cards[1]).x, 100.0, "box centered in slot"); + let o = ra + .aabb_of(cards[0]) + .intersection_area(&ra.aabb_of(cards[1])); + assert_close(o, 0.0, "no overlap (anchor arm)"); + + // sheet arm (VisualOnly): layout unchanged by rotation → overlap is correct + let rv = resolve(&doc, &opts_visual()); + assert_close(rv.box_of(f).w, 220.0, "container ignores rotation"); + assert_close(rv.box_of(cards[2]).x, 150.0, "siblings don't move"); + let ov = rv + .aabb_of(cards[0]) + .intersection_area(&rv.aabb_of(cards[1])); + assert_close(ov, 600.0, "overlap exists and is exact (visual-only arm)"); +} + +/// R-E1: quadrant angles produce bit-clean matrices end-to-end. +#[test] +fn re1_quadrant_angles_bit_clean() { + let mut b = DocBuilder::new(); + let (mut h, p) = shape(100.0, 50.0); + h.rotation = 90.0; + let s = b.add(0, h, p); + let doc = b.build(); + let w = run(&doc).world_of(s); + assert_eq!(w.a, 0.0); + assert_eq!(w.b, 1.0); + assert_eq!(w.c, -1.0); + assert_eq!(w.d, 0.0); +} + +/// Derived-box pivot (§5): a group rotates about the point its bindings +/// place, and the center-feel gesture compensates in exactly 3 writes. +#[test] +fn group_origin_pivot_and_center_feel_gesture() { + let mut b = DocBuilder::new(); + let mut gh = Header::new(SizeIntent::Auto, SizeIntent::Auto); + gh.x = AxisBinding::start(100.0); + gh.y = AxisBinding::start(50.0); + let g = b.add(0, gh, Payload::Group); + let (s1h, s1p) = shape(40.0, 40.0); + b.add(g, s1h, s1p); + let (mut s2h, s2p) = shape(40.0, 40.0); + s2h.x = AxisBinding::start(56.0); + b.add(g, s2h, s2p); + let mut doc = b.build(); + + let r0 = run(&doc); + let b0 = r0.box_of(g); + assert_rect(b0, 100.0, 50.0, 96.0, 40.0, "union box"); + // origin pivot: bindings-placed point is fixed under raw rotation write + doc.get_mut(g).header.rotation = 30.0; + let r1 = run(&doc); + let origin_before = r0.world_of(g).apply((0.0, 0.0)); + let origin_after = r1.world_of(g).apply((0.0, 0.0)); + assert_close(origin_before.0, origin_after.0, "pivot x fixed"); + assert_close(origin_before.1, origin_after.1, "pivot y fixed"); + + // center-feel gesture: 3 writes, union center fixed in parent space + doc.get_mut(g).header.rotation = 0.0; + let r2 = run(&doc); + let center_before = (b0.x + b0.w / 2.0, b0.y + b0.h / 2.0); + let writes = n0_model::ops::rotate_derived_center_feel(&mut doc, &r2, g, 30.0).unwrap(); + assert_eq!(writes, 3, "rotation + x + y — the Figma trick over scalars"); + let r3 = run(&doc); + let bb = r3.box_of(g); + let local_center = (bb.w / 2.0, bb.h / 2.0); + let center_after = r3.world_of(g).apply(local_center); + assert_close(center_after.0, center_before.0, "center x fixed"); + assert_close(center_after.1, center_before.1, "center y fixed"); +} diff --git a/crates/n0-model/tests/svg_animation.rs b/crates/n0-model/tests/svg_animation.rs new file mode 100644 index 00000000..e2ec7e32 --- /dev/null +++ b/crates/n0-model/tests/svg_animation.rs @@ -0,0 +1,1459 @@ +//! Retained SVG Animation Profiles 0–3 frontend/compiler contract. + +use n0_model::animation::{ + AnimationProgram, CompositeOperation, Easing, IterationCompositeOperation, SampleTime, +}; +use n0_model::model::{AnchorEdge, AxisBinding, SizeIntent}; +use n0_model::properties::{PropertyKey, PropertyValue}; +use n0_model::svg_animation::{ + SourceSnapshot, SvgAnimationSource, PROFILE0_COMPILER_ID, PROFILE1_COMPILER_ID, + PROFILE2_COMPILER_ID, PROFILE3_COMPILER_ID, +}; + +const BOUNDARIES: &str = + include_str!("../../../fixtures/test-svg/animation/svg-animation-profile0-boundaries.svg"); +const KEYFRAME_BOUNDARIES: &str = include_str!( + "../../../fixtures/test-svg/animation/svg-animation-profile1-keyframe-boundaries.svg" +); +const SANDWICH_BOUNDARIES: &str = include_str!( + "../../../fixtures/test-svg/animation/svg-animation-profile2-sandwich-boundaries.svg" +); +const ADDITIVE_BOUNDARIES: &str = include_str!( + "../../../fixtures/test-svg/animation/svg-animation-profile3-additive-boundaries.svg" +); +const DEMO: &str = + include_str!("../../../fixtures/test-svg/animation/svg-animation-profile0-demo.svg"); +const KEYFRAME_DEMO: &str = + include_str!("../../../fixtures/test-svg/animation/svg-animation-profile1-keyframes.svg"); +const SANDWICH_DEMO: &str = include_str!( + "../../../fixtures/test-svg/animation/svg-animation-profile2-replacement-sandwich.svg" +); +const ADDITIVE_DEMO: &str = + include_str!("../../../fixtures/test-svg/animation/svg-animation-profile3-motion-mixer.svg"); + +fn materialize(source: &str) -> SvgAnimationSource { + SvgAnimationSource::parse(SourceSnapshot::new("inline.svg", source)).unwrap() +} + +fn compile_error(source: &str) -> String { + match SvgAnimationSource::parse(SourceSnapshot::new("invalid.svg", source)) { + Ok(materialized) => materialized.compile_profile0().unwrap_err().to_string(), + Err(error) => error.to_string(), + } +} + +fn compile_profile1_error(source: &str) -> String { + match SvgAnimationSource::parse(SourceSnapshot::new("invalid-profile1.svg", source)) { + Ok(materialized) => materialized.compile_profile1().unwrap_err().to_string(), + Err(error) => error.to_string(), + } +} + +fn compile_profile2_error(source: &str) -> String { + match SvgAnimationSource::parse(SourceSnapshot::new("invalid-profile2.svg", source)) { + Ok(materialized) => materialized.compile_profile2().unwrap_err().to_string(), + Err(error) => error.to_string(), + } +} + +fn compile_profile3_error(source: &str) -> String { + match SvgAnimationSource::parse(SourceSnapshot::new("invalid-profile3.svg", source)) { + Ok(materialized) => materialized.compile_profile3().unwrap_err().to_string(), + Err(error) => error.to_string(), + } +} + +fn sampled_scalar_with( + source: &str, + time_ns: i64, + compile: impl FnOnce(&SvgAnimationSource) -> AnimationProgram, +) -> Option { + let materialized = materialize(source); + let program = compile(&materialized); + let target = program.tracks()[0].target(); + let values = program + .sample( + materialized.document(), + SampleTime::from_nanoseconds(time_ns), + ) + .unwrap(); + match values.get(target) { + Some(PropertyValue::AxisBinding(AxisBinding::Pin { + anchor: AnchorEdge::Start, + offset, + })) => Some(*offset), + Some(PropertyValue::SizeIntent(SizeIntent::Fixed(value))) => Some(*value), + Some(PropertyValue::Number(value)) => Some(*value), + None => None, + value => panic!("unexpected sampled scalar value {value:?}"), + } +} + +fn sampled_scalar(source: &str, time_ns: i64) -> Option { + sampled_scalar_with(source, time_ns, |materialized| { + materialized.compile_profile0().unwrap() + }) +} + +fn sampled_profile1_scalar(source: &str, time_ns: i64) -> Option { + sampled_scalar_with(source, time_ns, |materialized| { + materialized.compile_profile1().unwrap() + }) +} + +fn sampled_profile2_scalar(source: &str, time_ns: i64) -> Option { + sampled_scalar_with(source, time_ns, |materialized| { + materialized.compile_profile2().unwrap() + }) +} + +fn sampled_profile3_scalar(source: &str, time_ns: i64) -> Option { + sampled_scalar_with(source, time_ns, |materialized| { + materialized.compile_profile3().unwrap() + }) +} + +#[test] +fn retained_source_materializes_once_and_compiles_typed_tracks() { + let source = r##" + + + + + + + +"##; + let materialized = materialize(source); + assert_eq!(materialized.snapshot().source(), source); + assert_eq!(materialized.snapshot().identity(), "inline.svg"); + assert_eq!(materialized.viewport(), (120.0, 64.0)); + assert!(materialized.has_animation_markup()); + assert_eq!( + materialized + .document() + .get(materialized.document().root) + .children + .len(), + 2 + ); + + let program = materialized.compile_profile0().unwrap(); + assert_eq!(program.compiler_id(), PROFILE0_COMPILER_ID); + assert_eq!(program.tracks().len(), 2); + assert_eq!(program.tracks()[0].target().property, PropertyKey::X); + assert_eq!(program.tracks()[1].target().property, PropertyKey::Opacity); + + let values = program + .sample( + materialized.document(), + SampleTime::from_nanoseconds(500_000_000), + ) + .unwrap(); + assert!(values.iter().any(|(target, value)| { + target.property == PropertyKey::X + && matches!( + value, + PropertyValue::AxisBinding(AxisBinding::Pin { + anchor: AnchorEdge::Start, + offset + }) if *offset == 30.0 + ) + })); + assert!(values.iter().any(|(target, value)| { + target.property == PropertyKey::Opacity + && matches!(value, PropertyValue::Number(value) if *value == 0.5) + })); +} + +#[test] +fn all_five_properties_project_to_typed_midpoint_values() { + let source = r##" + + + + + + + + + +"##; + let materialized = materialize(source); + let program = materialized.compile_profile0().unwrap(); + let values = program + .sample( + materialized.document(), + SampleTime::from_nanoseconds(500_000_000), + ) + .unwrap(); + + for track in program.tracks() { + let value = values.get(track.target()).unwrap(); + match (track.target().property, value) { + (PropertyKey::X, PropertyValue::AxisBinding(AxisBinding::Pin { offset: 30.0, .. })) + | (PropertyKey::Y, PropertyValue::AxisBinding(AxisBinding::Pin { offset: 18.0, .. })) + | (PropertyKey::Width, PropertyValue::SizeIntent(SizeIntent::Fixed(40.0))) + | (PropertyKey::Height, PropertyValue::SizeIntent(SizeIntent::Fixed(24.0))) + | (PropertyKey::Opacity, PropertyValue::Number(0.5)) => {} + other => panic!("unexpected midpoint projection {other:?}"), + } + } +} + +#[test] +fn direct_seeks_are_order_independent_and_preserve_source_and_document() { + let materialized = materialize(BOUNDARIES); + let source_before = materialized.snapshot().source().to_string(); + let document_before = materialized.document().clone(); + let program = materialized.compile_profile0().unwrap(); + let times = [10, 17, 18, 25, 26, 9]; + let forward = times.map(|time| { + program + .sample(materialized.document(), SampleTime::from_nanoseconds(time)) + .unwrap() + }); + let shuffled = [26, 18, 9, 25, 10, 17]; + for time in shuffled { + let direct = program + .sample(materialized.document(), SampleTime::from_nanoseconds(time)) + .unwrap(); + let expected = forward[times + .iter() + .position(|candidate| *candidate == time) + .unwrap()] + .clone(); + assert_eq!(direct, expected, "seek at {time}ns"); + } + assert_eq!(materialized.snapshot().source(), source_before); + assert_eq!(materialized.document(), &document_before); +} + +#[test] +fn unsupported_dynamic_markup_is_retained_for_base_but_refused_for_sample() { + let source = r##" + + + + +"##; + let materialized = materialize(source); + assert_eq!(materialized.snapshot().source(), source); + assert!(materialized.has_animation_markup()); + assert_eq!( + materialized + .document() + .get(materialized.document().root) + .children + .len(), + 1 + ); + let error = materialized.compile_profile0().unwrap_err().to_string(); + assert!( + error.contains("recognizes only and "), + "{error}" + ); +} + +#[test] +fn parent_and_same_document_href_targeting_are_equivalent() { + let parent = r#" + + + + + +"#; + let href = r##" + + + + +"##; + assert_eq!(sampled_scalar(parent, 250_000_000), Some(14.0)); + assert_eq!(sampled_scalar(href, 250_000_000), Some(14.0)); +} + +#[test] +fn checked_boundary_fixture_hits_every_repeat_edge_exactly() { + let expected: [(i64, Option); 9] = [ + (9, None), + (10, Some(20.0)), + (11, Some(22.0)), + (17, Some(34.0)), + (18, Some(20.0)), + (19, Some(22.0)), + (25, Some(34.0)), + (26, Some(36.0)), + (27, Some(36.0)), + ]; + for (time, value) in expected { + assert_eq!(sampled_scalar(BOUNDARIES, time), value, "at {time}ns"); + } +} + +#[test] +fn profile1_is_bit_compatible_with_every_profile0_spelling() { + let direct_rounding = r#" + + + + + +"#; + for (source, times) in [ + (BOUNDARIES, &[-1, 9, 10, 11, 17, 18, 19, 25, 26, 27][..]), + ( + direct_rounding, + &[ + -1, 1_999_999, 2_000_000, 3_000_000, 9_000_000, 10_000_000, 11_000_000, 17_000_000, + 18_000_000, 19_000_000, + ][..], + ), + ] { + for &time in times { + assert_eq!( + sampled_scalar(source, time).map(f32::to_bits), + sampled_profile1_scalar(source, time).map(f32::to_bits), + "Profile 1 changed Profile 0 output at {time}ns" + ); + } + } +} + +#[test] +fn profile2_is_bit_compatible_with_profile1_single_effects() { + for (source, times) in [ + (BOUNDARIES, &[-1, 9, 10, 11, 17, 18, 19, 25, 26, 27][..]), + ( + KEYFRAME_BOUNDARIES, + &[ + -1, + 0, + 499_999_999, + 500_000_000, + 999_999_999, + 1_000_000_000, + 2_000_000_000, + 3_000_000_000, + ][..], + ), + ] { + for &time in times { + assert_eq!( + sampled_profile2_scalar(source, time).map(f32::to_bits), + sampled_profile1_scalar(source, time).map(f32::to_bits), + "Profile 2 changed Profile 1 output at {time}ns" + ); + } + } +} + +#[test] +fn profile2_orders_replacement_sandwiches_and_falls_through_exactly() { + let materialized = materialize(SANDWICH_BOUNDARIES); + let program = materialized.compile_profile2().unwrap(); + assert_eq!(program.compiler_id(), PROFILE2_COMPILER_ID); + assert_eq!(program.tracks().len(), 2); + assert_eq!(program.effect_stacks().count(), 1); + assert!(program.tracks()[0].source().contains("lower")); + assert!(program.tracks()[1].source().contains("higher")); + + let expected = [ + (0, None), + (500_000_000, None), + (999_999_999, None), + (1_000_000_000, Some(16.0)), + (1_000_000_001, Some(16.0)), + (1_500_000_000, Some(21.0)), + (1_999_999_999, Some(26.0)), + (2_000_000_000, Some(72.0)), + (2_000_000_001, Some(72.0)), + (2_500_000_000, Some(80.0)), + (2_999_999_999, Some(88.0)), + (3_000_000_000, Some(36.0)), + (3_000_000_001, Some(36.0)), + (3_500_000_000, Some(41.0)), + (4_000_000_000, Some(46.0)), + (4_500_000_000, Some(51.0)), + (4_999_999_999, Some(56.0)), + (5_000_000_000, Some(56.0)), + (5_000_000_001, Some(56.0)), + (6_000_000_000, Some(56.0)), + ]; + for (time, expected) in expected { + let actual = sampled_profile2_scalar(SANDWICH_BOUNDARIES, time); + assert_eq!(actual, expected, "at {time}ns"); + } +} + +#[test] +fn profile2_uses_later_document_order_only_for_equal_begins() { + let source = r#" + + + + + + +"#; + assert_eq!(sampled_profile2_scalar(source, 2_000_000_000), Some(26.0)); +} + +#[test] +fn profile2_repeat_boundaries_do_not_reprioritize_a_lower_effect() { + let source = r#" + + + + + + +"#; + + for (time, expected) in [ + (1_999_999_999, 205.0), + (2_000_000_000, 205.0), + (2_000_000_001, 205.0), + (2_999_999_999, 215.0), + (3_000_000_000, 215.0), + (3_000_000_001, 215.0), + (3_499_999_999, 220.0), + (3_500_000_000, 80.0), + (3_500_000_001, 80.0), + ] { + assert_eq!(sampled_profile2_scalar(source, time), Some(expected)); + } +} + +#[test] +fn profile2_repeats_sample_exact_boundaries_and_freeze_keeps_masking() { + let repeat = r#" + + + + + + +"#; + for (time, expected) in [ + (1_999_999_999, 18.0), + (2_000_000_000, 70.0), + (2_000_000_001, 70.0), + (2_999_999_999, 90.0), + (3_000_000_000, 70.0), + (3_000_000_001, 70.0), + (3_999_999_999, 90.0), + (4_000_000_000, 34.0), + (4_000_000_001, 34.0), + ] { + assert_eq!(sampled_profile2_scalar(repeat, time), Some(expected)); + } + + let freeze = repeat.replace("fill=\"remove\"", "fill=\"freeze\""); + for time in [4_000_000_000, 4_000_000_001] { + assert_eq!(sampled_profile2_scalar(&freeze, time), Some(90.0)); + } + assert_eq!(sampled_profile2_scalar(&freeze, 6_000_000_000), Some(90.0)); +} + +#[test] +fn profile2_remains_replacement_only() { + for attribute in ["additive=\"sum\"", "accumulate=\"sum\""] { + let source = format!( + r#""# + ); + let error = compile_profile2_error(&source); + assert!(error.contains("must be"), "{error}"); + assert!( + error.contains("replace") || error.contains("none"), + "{error}" + ); + } +} + +#[test] +fn profile3_is_cumulative_and_lowers_svg_composition_to_typed_operations() { + for source in [KEYFRAME_BOUNDARIES, SANDWICH_BOUNDARIES] { + for time in [ + -1, + 0, + 499_999_999, + 500_000_000, + 999_999_999, + 1_000_000_000, + 1_500_000_000, + 1_999_999_999, + 2_000_000_000, + 2_500_000_000, + 2_999_999_999, + 3_000_000_000, + 3_500_000_000, + 4_000_000_000, + 4_500_000_000, + 4_999_999_999, + 5_000_000_000, + 6_000_000_000, + ] { + assert_eq!( + sampled_profile3_scalar(source, time).map(f32::to_bits), + sampled_profile2_scalar(source, time).map(f32::to_bits), + "Profile 3 changed Profile 2 output at {time}ns" + ); + } + } + + let materialized = materialize(ADDITIVE_BOUNDARIES); + let program = materialized.compile_profile3().unwrap(); + assert_eq!(program.compiler_id(), PROFILE3_COMPILER_ID); + assert_eq!(program.tracks().len(), 4); + assert_eq!(program.effect_stacks().count(), 1); + let sources = program + .tracks() + .iter() + .map(|track| { + let source = track.source(); + let id = [ + "base-cumulative", + "lower-replace", + "upper-cumulative", + "upper-temporary", + ] + .into_iter() + .find(|id| source.contains(id)) + .unwrap(); + (id, track.composite(), track.iteration_composite()) + }) + .collect::>(); + assert_eq!( + sources, + vec![ + ( + "base-cumulative", + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + ), + ( + "lower-replace", + CompositeOperation::Replace, + IterationCompositeOperation::Replace, + ), + ( + "upper-cumulative", + CompositeOperation::Add, + IterationCompositeOperation::Accumulate, + ), + ( + "upper-temporary", + CompositeOperation::Add, + IterationCompositeOperation::Replace, + ), + ] + ); + + for (time, expected) in [ + (0, 10.0), + (500_000_000, 12.0), + (1_000_000_000, 16.0), + (1_500_000_000, 18.0), + (2_000_000_000, 22.0), + (2_500_000_000, 20.0), + (3_000_000_000, 25.0), + (3_500_000_000, 38.0), + (4_000_000_000, 51.0), + (4_500_000_000, 52.0), + (5_000_000_000, 61.0), + (5_500_000_000, 66.0), + (6_000_000_000, 71.0), + (6_500_000_000, 76.0), + (7_000_000_000, 76.0), + ] { + assert_eq!( + sampled_profile3_scalar(ADDITIVE_BOUNDARIES, time), + Some(expected), + "at {time}ns" + ); + } +} + +#[test] +fn profile3_lowers_replacement_accumulation_independently() { + let source = r#" + + + + + +"#; + let materialized = materialize(source); + let program = materialized.compile_profile3().unwrap(); + assert_eq!(program.tracks().len(), 1); + assert_eq!(program.tracks()[0].composite(), CompositeOperation::Replace); + assert_eq!( + program.tracks()[0].iteration_composite(), + IterationCompositeOperation::Accumulate + ); + + for (time, expected) in [ + (0, 20.0), + (1_000_000_000, 50.0), + (1_500_000_000, 55.0), + (3_000_000_000, 90.0), + ] { + assert_eq!(sampled_profile3_scalar(source, time), Some(expected)); + } +} + +#[test] +fn profile3_has_exact_additive_repeat_and_fallthrough_boundaries() { + for (time, expected) in [ + (999_999_999, 14.0), + (1_000_000_000, 16.0), + (2_499_999_999, 24.0), + (2_500_000_000, 20.0), + (3_499_999_999, 34.0), + (3_500_000_000, 38.0), + (4_499_999_999, 64.0), + (4_500_000_000, 52.0), + ] { + assert_eq!( + sampled_profile3_scalar(ADDITIVE_BOUNDARIES, time), + Some(expected), + "at {time}ns" + ); + } +} + +#[test] +fn profile3_clamps_only_the_final_opacity_sandwich() { + let source = r#" + + + + + + +"#; + assert_eq!(sampled_profile3_scalar(source, 0), Some(1.0)); +} + +#[test] +fn profile3_rejects_unknown_composition_values_without_weakening_older_profiles() { + for (attribute, accepted) in [ + ("additive=\"multiply\"", "replace"), + ("accumulate=\"multiply\"", "none"), + ] { + let source = format!( + r#""# + ); + let error = compile_profile3_error(&source); + assert!(error.contains(accepted), "{error}"); + assert!(error.contains("sum"), "{error}"); + } + + let additive = ADDITIVE_BOUNDARIES.replace("additive=\"sum\"", "additive=\"replace\""); + let error = compile_profile2_error(&additive); + assert!(error.contains("accumulate"), "{error}"); + assert!(error.contains("none"), "{error}"); +} + +#[test] +fn profile1_values_and_key_times_lower_to_one_canonical_curve() { + let source = r#" + + + + + +"#; + let materialized = materialize(source); + let program = materialized.compile_profile1().unwrap(); + assert_eq!(program.compiler_id(), PROFILE1_COMPILER_ID); + let curve = program.tracks()[0].scalar_curve().unwrap(); + assert_eq!(curve.keyframe_count(), 3); + assert_eq!(curve.keyframes().nth(1).unwrap().offset().numerator(), 1); + assert_eq!(curve.keyframes().nth(1).unwrap().offset().denominator(), 4); + assert!(curve + .segments() + .iter() + .all(|segment| segment.easing() == Easing::Linear)); + + assert_eq!(sampled_profile1_scalar(source, 0), Some(10.0)); + assert_eq!(sampled_profile1_scalar(source, 250_000_000), Some(30.0)); + assert_eq!(sampled_profile1_scalar(source, 625_000_000), Some(60.0)); + assert_eq!(sampled_profile1_scalar(source, 1_000_000_000), Some(90.0)); +} + +#[test] +fn profile1_implicit_equal_times_match_their_explicit_spelling() { + let implicit = r#" + + + + + +"#; + let explicit = implicit.replace( + "calcMode=\"spline\"", + "keyTimes=\"0;0.5;1\" calcMode=\"spline\"", + ); + for time in [ + 0, + 1, + 249_999_999, + 250_000_000, + 499_999_999, + 500_000_000, + 500_000_001, + 750_000_000, + 999_999_999, + 1_000_000_000, + ] { + assert_eq!( + sampled_profile1_scalar(implicit, time).map(f32::to_bits), + sampled_profile1_scalar(&explicit, time).map(f32::to_bits), + "implicit and explicit equal offsets differ at {time}ns" + ); + } +} + +#[test] +fn profile1_constant_and_spline_modes_have_closed_semantics() { + let constant = r#" + + + + + +"#; + for time in [0, 1, 999_999_999, 1_000_000_000, i64::MAX] { + assert_eq!(sampled_profile1_scalar(constant, time), Some(17.0)); + } + + let spline = r#" + + + + + +"#; + let quarter = sampled_profile1_scalar(spline, 250_000_000).unwrap(); + let midpoint = sampled_profile1_scalar(spline, 500_000_000).unwrap(); + let three_quarters = sampled_profile1_scalar(spline, 750_000_000).unwrap(); + assert!(quarter < 25.0, "ease-in quarter was {quarter}"); + assert_eq!(midpoint.to_bits(), 50.0_f32.to_bits()); + assert!( + three_quarters > 75.0, + "ease-out quarter was {three_quarters}" + ); + + let ignored_linear_splines = r#" + + + + + +"#; + assert_eq!( + sampled_profile1_scalar(ignored_linear_splines, 250_000_000) + .unwrap() + .to_bits(), + 25.0_f32.to_bits(), + "valid keySplines do not affect linear mode or impose a tuple count" + ); +} + +#[test] +fn profile1_spline_controls_round_directly_to_binary32() { + // This decimal is just above the midpoint between 0.5f32 and its next + // representable value. Parsing through binary64 first rounds to the exact + // midpoint and would then choose 0.5 by ties-to-even; direct binary32 + // parsing correctly chooses the upper value. + let source = r#" + + + + + +"#; + let materialized = materialize(source); + let program = materialized.compile_profile1().unwrap(); + let easing = program.tracks()[0].scalar_curve().unwrap().segments()[0].easing(); + let Easing::CubicBezier(spline) = easing else { + panic!("expected cubic Bézier easing"); + }; + assert_eq!(spline.x1().to_bits(), 0x3f00_0001); +} + +#[test] +fn profile1_keyframe_fixture_preserves_exact_boundaries_repeats_and_freeze() { + let expected: [(i64, Option); 10] = [ + (9, None), + (10, Some(4.0)), + (13, Some(16.0)), + (14, Some(20.0)), + (15, Some(22.666666)), + (26, Some(4.0)), + (30, Some(20.0)), + (41, Some(49.333332)), + (42, Some(52.0)), + (43, Some(52.0)), + ]; + for (time, expected) in expected { + let actual = sampled_profile1_scalar(KEYFRAME_BOUNDARIES, time); + match (actual, expected) { + (Some(actual), Some(expected)) => assert_eq!( + actual.to_bits(), + expected.to_bits(), + "keyframe boundary sample at {time}ns" + ), + (actual, expected) => assert_eq!(actual, expected, "sample at {time}ns"), + } + } +} + +#[test] +fn profile1_rejects_ambiguous_or_malformed_keyframe_grammar() { + let cases = [ + ( + r#""#, + "values on", + ), + ( + r#""#, + "values[1]", + ), + ( + r#""#, + "values[0]", + ), + ( + r#""#, + "has 2 entries; values has 3", + ), + ( + r#""#, + "first keyframe offset must be 0", + ), + ( + r#""#, + "last keyframe offset must be 1", + ), + ( + r#""#, + "increase strictly", + ), + ( + r#""#, + "increase strictly", + ), + ( + r#""#, + "requires `keySplines`", + ), + ( + r#""#, + "has 1 entries; 2 required", + ), + ( + r#""#, + "must be in [0, 1] before binary32 rounding", + ), + ( + r#""#, + "one-value constant", + ), + ( + r#""#, + "valid SVG but unsupported by Profile 1", + ), + ( + r#""#, + "control 0 must be an SVG number", + ), + ( + r#""#, + "requires a comma or XML whitespace", + ), + ( + r#""#, + "ends with a comma", + ), + ( + r#""#, + "before binary32 rounding", + ), + ]; + + for (animation, expected) in cases { + let source = format!( + r#"{animation}"# + ); + let error = compile_profile1_error(&source); + assert!( + error.contains(expected), + "expected `{expected}` in `{error}`" + ); + assert!(error.starts_with("invalid-profile1.svg:1:"), "{error}"); + } +} + +#[test] +fn profile1_source_limits_close_the_new_list_and_exact_number_domains() { + let document = |animation: &str| { + format!( + r#"{animation}"# + ) + }; + + let values_at_limit = (0..4_096) + .map(|index| if index == 4_095 { "1" } else { "0" }) + .collect::>() + .join(";"); + let source = document(&format!( + r#""# + )); + assert_eq!( + materialize(&source).compile_profile1().unwrap().tracks()[0] + .scalar_curve() + .unwrap() + .keyframe_count(), + 4_096 + ); + + let values_over_limit = (0..4_097).map(|_| "0").collect::>().join(";"); + let source = document(&format!( + r#""# + )); + assert!( + compile_profile1_error(&source).contains("profile limit is 4096"), + "values limit must fail explicitly" + ); + + let splines_at_limit = (0..4_095).map(|_| "0 0 1 1").collect::>().join(";"); + let source = document(&format!( + r#""# + )); + materialize(&source).compile_profile1().unwrap(); + + let splines_over_limit = format!("{splines_at_limit};0 0 1 1"); + let source = document(&format!( + r#""# + )); + assert!( + compile_profile1_error(&source).contains("profile limit is 4095"), + "spline-list limit must fail explicitly" + ); + + let control_128 = format!("1.{}", "0".repeat(126)); + let source = document(&format!( + r#""# + )); + materialize(&source).compile_profile1().unwrap(); + + let control_129 = format!("1.{}", "0".repeat(127)); + let source = document(&format!( + r#""# + )); + assert!( + compile_profile1_error(&source) + .contains("control 3 is 129 bytes; the profile limit is 128"), + "spline-control token limit must fail explicitly" + ); + + let key_time_128 = format!("1.{}", "0".repeat(126)); + let source = document(&format!( + r#""# + )); + materialize(&source).compile_profile1().unwrap(); + + let key_time_129 = format!("1.{}", "0".repeat(127)); + let source = document(&format!( + r#""# + )); + assert!( + compile_profile1_error(&source).contains("is 129 bytes; the profile limit is 128"), + "key-time token limit must fail explicitly" + ); + + for (middle, accepted) in [ + ("0.0000000000000000001", true), + ("0.00000000000000000001", false), + ] { + let source = document(&format!( + r#""# + )); + if accepted { + materialize(&source).compile_profile1().unwrap(); + } else { + assert!( + compile_profile1_error(&source).contains("unsigned 64-bit denominator domain"), + "reduced-ratio boundary must fail explicitly" + ); + } + } + + for exponent in [128, -128] { + let source = document(&format!( + r#""# + )); + materialize(&source).compile_profile1().unwrap(); + let source = document(&format!( + r#""# + )); + materialize(&source).compile_profile1().unwrap(); + } + for exponent in [129, -129] { + let expected = format!("decimal exponent must be in [-128, 128]; found {exponent}"); + let source = document(&format!( + r#""# + )); + assert!( + compile_profile1_error(&source).contains(&expected), + "key-time exponent limit must fail explicitly" + ); + let source = document(&format!( + r#""# + )); + assert!( + compile_profile1_error(&source).contains(&expected), + "spline exponent limit must fail explicitly" + ); + } +} + +#[test] +fn authored_property_domains_are_checked_before_binary32_rounding() { + let document = |animation: &str| { + format!( + r#"{animation}"# + ) + }; + + for animation in [ + r#""#, + r#""#, + ] { + let source = document(animation); + let profile0 = compile_error(&source); + let profile1 = compile_profile1_error(&source); + assert!(profile0.contains("before binary32 rounding"), "{profile0}"); + assert!(profile1.contains("before binary32 rounding"), "{profile1}"); + } + + for animation in [ + r#""#, + r#""#, + ] { + let error = compile_profile1_error(&document(animation)); + assert!(error.contains("before binary32 rounding"), "{error}"); + } + + let signed_zero = + document(r#""#); + materialize(&signed_zero).compile_profile0().unwrap(); + materialize(&signed_zero).compile_profile1().unwrap(); +} + +#[test] +fn profiles_reject_negative_begin_before_format_neutral_timing() { + let source = r#" + + + + + +"#; + + for error in [compile_error(source), compile_profile1_error(source)] { + assert!( + error.contains( + "begin on at 4:5 must use the exact non-negative clock grammar" + ), + "{error}" + ); + } +} + +#[test] +fn namespace_prefixes_do_not_change_svg_element_identity() { + let prefixed = r#" + + + + + +"#; + assert_eq!(sampled_scalar(prefixed, 500_000_000), Some(24.0)); +} + +#[test] +fn sample_is_strict_about_side_channels_targeting_and_grammar() { + let cases = [ + ( + r#""#, + "not in the SVG namespace", + ), + ( + r#""#, + "viewBox is not accepted", + ), + ( + r#""#, + "unknown attribute `transform`", + ), + ( + r#""#, + "bare same-document fragment", + ), + ( + r##""##, + "unsupported attribute `xlink:href`", + ), + ( + r#""#, + "requires `dur`", + ), + ( + r#""#, + "decimal digits only", + ), + ( + r#""#, + "non-negative for size animation", + ), + ( + r#""#, + "between 0 and 1 inclusive", + ), + ( + r#""#, + "admits only ", + ), + ( + r#""#, + "