@hatiolab/figure-model 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ # V3 shape-dimension contract — per-axis radii, elliptic sections, and dimensions that follow state
2
+
3
+ Status: **contract, fixed 2026-09-22 by the V3 designer** (ADR-0087, stage-review rulings 3 and 5). §1 (per-axis
4
+ dimensions) implemented the same day: `cylinder-shape@2`, `rounded-box@2`, `hollow-box@2`, the converter writing
5
+ them, the sampler reading them. §2–3 implemented the same night: dependence on state is tracked per value in the
6
+ compiler and printed as `dependence` beside `outputTypes`; every operator carries a `state` policy (`follows` or
7
+ `refuses`; an operator without one fails with `STATE_POLICY`); `occupancy.extent` may not depend on state
8
+ (`OCCUPANCY_STATE`); `inspectV3Asset` covers the state range and reports `proven` (corners; no state-dependent
9
+ rotation), `sampled` (a rotation depends on state; corners plus per-input sweeps, step stated, not a proof) or
10
+ `defaults` (one state, as asked). The `bounded` method of §3 (swept envelope) is not implemented; such assets report
11
+ `sampled`. The converter writes a two-key scale channel as dimensions × affine(parameter) for percent or ratio
12
+ parameters (`STATE_DIMENSION` note). This is one rule for what a shape's dimensions are and what they may depend on. It is not three
13
+ exceptions bolted on for the three assets that needed them.
14
+
15
+ ## What a person sees
16
+
17
+ An author opens the electrode roll cradle. A "fed" value sits at 0 %. As they drag it toward 100 %, the roll on the
18
+ cradle gets thinner: its radius shrinks, its length does not. The edge plates that ride on the roll shrink with it and
19
+ stay on its surface. Nothing else moves. At 0 % the roll is exactly the drawn roll.
20
+
21
+ An author stretches a kiosk to twice its width. The bezel's corners stay the same round in front view where the
22
+ section did not stretch, and go wider than they are tall where it did, because V2 drew them that way. Nothing pops
23
+ back to a circle on its own.
24
+
25
+ An author opens the worker figure and stretches it sideways. The legs, which the author chose not to keep round,
26
+ become oval like V2 drew them. The renderer draws the oval it is given. It never "fixes" it to a circle.
27
+
28
+ ## 1. Every round thing has per-axis dimensions
29
+
30
+ A section is round only when its two cross dimensions are equal. The providers write both:
31
+
32
+ | operator | evaluated provider | dimensions | round when | status |
33
+ |---|---|---|---|---|
34
+ | `cylinder-shape@2` | `ellipticCylinder` | `radiusX`, `radiusZ`, `length` (axis Y) | `radiusX = radiusZ` | implemented 2026-09-22 |
35
+ | `frustum-shape@2` | — | `topRadiusX`, `topRadiusZ`, `bottomRadiusX`, `bottomRadiusZ`, `height` | pairs equal | not implemented; no V2 shape needs it (V2 draws no frustum) |
36
+ | `sphere-shape@1` | `sphere` | `radiusX`, `radiusY`, `radiusZ` (already so) | all equal | implemented |
37
+ | `rounded-box@2` | `roundedBoxXZ` | `width`, `height`, `depth`, `roundX`, `roundZ` | `roundX = roundZ` | implemented 2026-09-22 |
38
+ | `hollow-box@2` | `hollowBoxXZ` | as `hollow-box@1` with `roundX`, `roundZ` in place of `round` | same | implemented 2026-09-22 |
39
+
40
+ The evaluated provider name is distinct from the `@1` one so a renderer can never mistake per-axis dimensions for a
41
+ single radius. Features on `ellipticCylinder` are `start` and `end` only; `radialBottom` has no single meaning on an
42
+ ellipse.
43
+
44
+ - `@1` of each provider stays valid and means the equal case; the converter writes `@2` only where V2 drew an
45
+ unequal section. Nothing that exists today is invalidated.
46
+ - **The renderer draws what it is given.** An elliptic section is an ellipse; an elliptic corner is the quadratic
47
+ Bézier from `(hw − roundX, −hd)` to `(hw, −hd + roundZ)` with the corner `(hw, −hd)` as control point, which is
48
+ what things-scene's `roundedRect` draws and what `src/v3-mesh-compare.ts` samples. It is not a circular or
49
+ elliptic arc: an arc differs from the Bézier by up to 0.06 × round at the corner's middle. A renderer that snaps a
50
+ section to a circle is wrong, and the mesh-level comparison of the completion criterion is what catches it.
51
+ - Validity: every dimension positive (a corner radius may be 0), `roundX ≤ width/2`, `roundZ ≤ depth/2`. A
52
+ declared corner larger than the section is refused at compile; the converter normalises V2's declared value to
53
+ what V2 drew (ruling 2) and reports it.
54
+
55
+ ### What `keepRound` becomes
56
+
57
+ V2's `keepRound` was a rule about **what happens to a round section under non-uniform sizing**: keep it round by
58
+ the geometric mean of the two factors. In V3 that is not a flag; it is two possible expressions for two dimensions:
59
+
60
+ ```
61
+ kept round radiusX = radiusZ = r₀ · geomean(fx, fz) (V2 keepRound on, the default)
62
+ follows axes radiusX = r₀ · fx, radiusZ = r₀ · fz (V2 keepRound off)
63
+ ```
64
+
65
+ Both are ordinary length expressions. The converter chooses by the V2 flag and writes the expression; the
66
+ `ELLIPTIC_SECTION` refusal is now a note on the report (implemented 2026-09-22), and `CORNER_PER_AXIS` notes a
67
+ corner written with two semi-axes. The same two shapes apply to corners: V2 scaled a rounded box's finished mesh
68
+ per axis, so a corner drawn with radius `r₀` becomes `roundX = r₀·fx`, `roundZ = r₀·fz`. That is what
69
+ `ROUND_NOT_SCALED` (40 parts in the last run) was reporting as a loss; with `@2` corners it is no longer a loss.
70
+
71
+ ## 2. A dimension may depend on state
72
+
73
+ A state input (`role: 'state'`) may be an argument of a shape or length node. A consumed roll's radius is
74
+ `r₀ − fed · k`; a lamp's glow radius grows with the gate. This is the core-semantics §2 invariant ("state changes the
75
+ actual shape dimension") made concrete, and it is what a V2 `scale` channel means: `dims × affine(p)`.
76
+
77
+ ### Dependence is tracked in the type, apart from unit and frame
78
+
79
+ Every value carries, next to its unit or frame, a **dependence set**: which state inputs its value can change with.
80
+ Inputs of role `state` start with `{self}`; constants and design inputs with `∅`; every operator's outputs carry the
81
+ union of its arguments' sets, unless the operator's contract says otherwise. This is the existing `stateful`
82
+ tracking in `compileV3Graph`, made a first-class part of the type and printed in `outputTypes`.
83
+
84
+ ### Every consuming operator declares its policy
85
+
86
+ An operator that receives a state-dependent argument does one of three things, **written in its contract**, and a
87
+ new operator that says nothing fails to compile the moment such an argument reaches it. There is no list of banned
88
+ operators to keep in sync.
89
+
90
+ | policy | meaning | operators |
91
+ |---|---|---|
92
+ | `follows` | the output depends on state; fine | shape providers, `rigid`, `compose`, `attach`, `axis-turn`, `axis-slide`, `add`, `mul`, `div`, `geomean`, `feature`, `place`, `member` |
93
+ | `refuses` | a state-dependent argument is a compile error | `fit-pitch@1`, `fixed-count@1` (count and pitch: structure and cost stay design-only; this is today's `TOPOLOGY_STATE`), the three lengths named by `occupancy.extent` |
94
+ | `bounded` | allowed, and the contract states the resource limit that holds over the whole state range | none today; the slot for a future layout that may follow state |
95
+
96
+ So: shape, attachment position and pose may follow state (a plate riding on the thinning roll follows it); the
97
+ declared occupancy is a state-independent contract; structure- and cost-changing operations say so themselves.
98
+
99
+ ## 3. The occupancy holds over the whole valid state range
100
+
101
+ `occupancy` is the contract a board's placement relies on (ADR-0087 decision 7). A volume that is true at the
102
+ default state and false at another is not that contract. So `inspectV3Asset` answers for the **whole** valid state
103
+ range, and says how it knows:
104
+
105
+ | how the geometry depends on state | how the check is done | what the report says |
106
+ |---|---|---|
107
+ | no state dependence | one evaluation at the defaults | `proven` |
108
+ | every state-dependent world coordinate is affine or monotone in each state input (shape dimensions from `add`/`mul`/`geomean` of a single state input; slides along an axis) | evaluate at every corner of the state box (each input at min and max); the extremes are at the corners | `proven`, listing the corners |
109
+ | a rotation or other non-linear composition is in the chain (`axis-turn` under a state input) | a conservative bound: the swept envelope of the turning subtree over the joint's range, or an extremum search on the offending coordinate with a stated step | `bounded`, with the method |
110
+ | neither is available | sample the range with a stated step | `sampled`, with the range and the samples; **never** worded as a whole-range guarantee |
111
+
112
+ - A violation found anywhere in the range is a violation. The gate never widens the occupancy to make it go away.
113
+ The author either declares a volume that covers the valid range, or narrows the allowed state range.
114
+ - The endpoint-only shortcut is **wrong** for a rotating long part: inside at both end angles, out in the middle.
115
+ That case takes the `bounded` row, not the `proven` one.
116
+
117
+ ## 4. What the converter writes with this contract
118
+
119
+ - V2 cylinder with `keepRound: false` and differing cross factors → `cylinder-shape@2` with `radiusX`, `radiusZ`
120
+ following their own axes. WORKER converts; `ELLIPTIC_SECTION` is retired.
121
+ - V2 rect or hollow rect whose section follows the instance → `rounded-box@2` / `hollow-box@2` with per-axis corner
122
+ radii `r·fx`, `r·fz`. `ROUND_NOT_SCALED` is retired.
123
+ - V2 `scale` channel on a part, two linear keys, from a parameter → each scaled dimension becomes
124
+ `dim × affine(p)`; the part's centre is unchanged (V2 scaled about the part centre). The dependence set of every
125
+ affected length includes the parameter. DRY_ROOM's lamps and ROLL_CRADLE's roll convert; `ANIMATION_CHANNEL`
126
+ stays only for channels the contract does not cover (pivots, compound axes, curves other than two linear keys).
127
+ - The report marks every state-dependent dimension it wrote, and the occupancy inspection of a converted asset
128
+ runs over the parameter ranges as §3 says.
129
+
130
+ ## 5. Completion criterion for this contract
131
+
132
+ - The four providers' `@2` forms compile, evaluate, and their `@1` forms are unchanged (the operator-list guard
133
+ gains the four names).
134
+ - Dependence appears in `outputTypes`; a synthetic new operator with no policy fails to compile when handed a
135
+ state-dependent value; `fit-pitch@1` and `fixed-count@1` still refuse it; a shape provider accepts it.
136
+ - The three assets convert and match V2 at box level at 8 sizes × states, and the WORKER's legs match as ovals.
137
+ - An occupancy check on the roll cradle reports `proven` over the `fed` range, and a synthetic rotating long part
138
+ reports `bounded`, with a violation found at a middle angle that the endpoints alone would have missed.
139
+ - The mesh-level comparison of the completion criterion covers sections and corners, so an elliptic section drawn
140
+ round would fail it.
141
+
142
+ ## Not in this contract
143
+
144
+ - Theme mapping and the colour cache (renderer work; ADR-0087 stage review ruling 4).
145
+ - Shape deformation beyond dimensions: bending, twisting, free-form profiles that change with state. Those would be
146
+ new providers with their own contracts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatiolab/figure-model",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Figure 저작 결과의 정본 형식과 검증 — 재질별로 병합되고 이름이 붙은 부품 그래프. 3D 로 저작한 형상 하나가 씬 컴포넌트로 서는 데 필요한 것을 담는다.",
5
5
  "keywords": [
6
6
  "figure",
@@ -62,6 +62,7 @@
62
62
  "access": "public"
63
63
  },
64
64
  "scripts": {
65
+ "version": "node scripts/sync-kernel-version.mjs && git add src/v3-kernel-version.ts",
65
66
  "build": "tsc -p tsconfig.build.json",
66
67
  "check": "tsc -p tsconfig.json --noEmit",
67
68
  "test": "npm run check && node --test src/*.test.ts",