uxml-preview 0.1.0 → 0.3.0

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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,171 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 — unreleased
3
+ ## 0.3.0 — Unreleased
4
+
5
+ Four fixes the VSCode extension (`vscode-uxml-preview`) hit while consuming
6
+ this package as a dependency — not speculative additions.
7
+
8
+ ### Added
9
+
10
+ - **`KNOWN_DIVERGENCES`** gives a host something to tell apart "a limit we
11
+ know about" from "a bug". Three entries, and the kinds carry the
12
+ distinction that matters. Font metrics move with the platform, so the
13
+ difference cannot be reproduced here at all. Unity's rule for a wrap
14
+ container's height is simply not specified, so there is nothing to match
15
+ yet. And the Yoga vendored in UI Toolkit resolves a main-axis percentage
16
+ against an indefinite parent differently from the one this package depends
17
+ on — that one reproduces perfectly every time; it just cannot be fixed
18
+ from inside this repository. The 1px case is deliberately absent: that is
19
+ the golden tests' judgement parameter, not a limit of the renderer, and a
20
+ host showing it would read as a defect.
21
+ - **`uxml-preview/unity-project`**, a Node-only subpath exporting
22
+ `buildGuidIndex(projectRoot)`. Builds a GUID → absolute-path index from a
23
+ Unity project's `.meta` files, for the case where an asset reference's path
24
+ has gone stale but its GUID is still good. Skips `Library/`, `Temp/`,
25
+ `obj/`, `.git/`, and any `.meta` file it cannot read. Kept out of the main
26
+ entry point because it uses `node:fs` and the main entry must stay
27
+ browser-safe.
28
+
29
+ ### Changed
30
+
31
+ - **`resolveAsset` now receives a second argument, `form: 'url' | 'resource'`**,
32
+ telling `url("...")` and `resource("...")` background-image references
33
+ apart. They resolve differently — `url()` is a path, `resource()` is a
34
+ Unity Resources-folder name — and a host that resolved both the same way
35
+ before this had no way to know which one it was looking at.
36
+ **Non-breaking**: existing `(path) => ...` implementations keep working
37
+ unchanged: `form` is an additional argument, not a replacement one.
38
+
39
+ ### Fixed
40
+
41
+ - `UxmlDocument.warnings` and `RenderResult.warnings` doc comments corrected.
42
+ The former said it covers "malformed input" only; it also carries
43
+ `'import-unresolved'` for an unreadable `<Style src="…">` or `@import`. The
44
+ latter said it is distinct from the former on that same wrong premise.
45
+ Comment-only — no behavior changed.
46
+
47
+ ### Docs
48
+
49
+ - **`docs/accuracy.md`'s published figures were stale**: two comparable cases
50
+ (`state-vs-id`, `state-vs-inline`) were merged after the doc's numbers were
51
+ last written, and nothing caught it. Recomputed from a live run rather than
52
+ trusted: **548 / 564 values (97.2%)**, **31 / 33 cases matching**, **33 / 33**
53
+ Unity baseline coverage — up from the stale 532/548, 29/31, 31/31. Mirrored
54
+ into `README.md` (both languages) and `docs/uss-vs-css.md`/`.ko.md`.
55
+ `docs/accuracy.en.md` was updated too but is not machine-checked (see next).
56
+ - **Added a regression guard**: `tests/golden/golden.test.ts` now has
57
+ `matches the figures published in docs/accuracy.md`, which recomputes the
58
+ three headline numbers from the actual case set and fails if
59
+ `docs/accuracy.md` says anything else — including failing (not skipping) if
60
+ it cannot find a row to check, so the guard can't be silently defeated by a
61
+ table-format change. This is the third time this specific number went stale
62
+ publicly (`docs/accuracy.md` itself already records the first two); the doc
63
+ fix alone would only prevent a third, not a fourth.
64
+ - Left unfixed and flagged in `docs/accuracy.md`/`accuracy.en.md`: the
65
+ "which controls this covers" breakdown table (`Button`/`ScrollView`/etc.
66
+ row counts) was already internally inconsistent before this change
67
+ (17+1+6+5+1 = 30, not the stated 31) and is not machine-checked. Recomputing
68
+ it requires a counting rule that is not written down anywhere; a guess
69
+ risked swapping one wrong number for another, so it was left alone and
70
+ called out inline instead.
71
+
72
+ ## 0.2.0 — 2026-08-06
73
+
74
+ Five controls instead of three, a representative screen measured end to end
75
+ against Unity, and two cascade defects that a synthetic case set could not have
76
+ found.
77
+
78
+ ### Changed — this affects what you see on screen
79
+
80
+ - **Controls with no renderer of their own are now drawn as plain boxes**
81
+ instead of being dropped along with everything inside them. One
82
+ `<ui:ScrollView>` used to empty a panel. Each still reports a warning, and
83
+ what is missing is only whatever makes that control look like itself.
84
+ - **`Button` now carries Unity's default `margin: 1px 3px`.** Existing layouts
85
+ containing buttons will shift by a few pixels — toward Unity, not away from
86
+ it. Unity supplies this through a theme stylesheet and this library supplied
87
+ nothing, which is why the first `Button` ever compared missed on 18 of 48
88
+ values. Overridable from your own USS exactly as in Unity, and the values are
89
+ in `src/controls/theme.ts`.
90
+ - **`Button` labels are now centred**, which is what Unity does. Found by the eye
91
+ check rather than by measurement: where a glyph sits inside a box does not move
92
+ the box, so all 548 compared coordinates agreed while the two screenshots
93
+ plainly did not.
94
+ - **Control defaults now lose to your USS regardless of specificity.** They are a
95
+ lower origin, like a browser's user-agent sheet — measured, and the opposite of
96
+ what this library previously did and documented. `Button { margin: 0 }` in your
97
+ stylesheet now wins against the built-in `.unity-button` rule, as in Unity.
98
+ - **`enabled="false"` puts an element and its subtree into `:disabled`**, without
99
+ anyone passing `states`. Both the state and its inheritance are measured.
100
+ - **Disabled elements render at half opacity**, as Unity draws them. The value
101
+ came from sampling the screenshots rather than judging them: Unity paints the
102
+ disabled button rgb(41,50,67), which is its base colour composited at exactly
103
+ one half over the footer on all three channels.
104
+
105
+ ### Added
106
+
107
+ - **`ScrollView`.** One tag becomes four elements in Unity, and the three it adds
108
+ decide where everything inside lands — reproducing the box alone put every
109
+ descendant in the wrong place and squeezed children that should have overflowed.
110
+ The hierarchy was observed from layout dumps rather than read from a source
111
+ mirror; the middle level, `unity-content-and-vertical-scroll-container`, appears
112
+ in no documentation. Scrollbar width is reserved but nothing is drawn: dragging,
113
+ wheeling and scroll position are outside a static render.
114
+ - **`Image`**, and `-unity-background-scale-mode` with its three values. The
115
+ default is `stretch-to-fill`, confirmed by the eye check.
116
+ - **Author USS reaches control parts.** `#unity-content-container { flex-direction: row }`
117
+ — how a real project lays out a scroll region's contents — previously reached
118
+ nothing at all, because parts were built after the cascade had run. A selector
119
+ naming any `unity-` element that matches nothing now warns and says why.
120
+ - **`<Style src="…">` is read**, through the same `resolveImport` hook as
121
+ `@import`. UI Builder writes that element into a file whenever a stylesheet is
122
+ attached, so it is the ordinary shape of a real document — and ignoring it meant
123
+ a real project's UXML rendered unstyled with nothing said about why.
124
+
125
+ - **Pseudo-class states, per element.** `render(doc, el, { states: { '#UseButton':
126
+ ['hover'], '#DropButton': ['disabled'] } })`. Keys are ordinary USS selectors.
127
+ States are explicit input rather than mouse events, so the same call always
128
+ draws the same picture — a screen you can compare against Unity, or against
129
+ yesterday. Per element rather than per document because real screens mix one
130
+ hovered button with a disabled one.
131
+ Unity's *own* state defaults are deliberately not included: a layout dump
132
+ measures geometry and a hover colour is not geometry, so there is no way to
133
+ prove them yet.
134
+ - Style provenance now records which states a rule needed (`origin.states`), so
135
+ an editor can tell "this is the hover colour" from "this is the colour".
136
+
137
+ ### Verified
138
+
139
+ **532 of 548 element coordinates identical** against Unity 6000.0.40f1 across 31
140
+ layout cases, one of which is a complete inventory screen.
141
+
142
+ The ratio is lower than 0.1.0's 242/244, and the reason is worth stating: that
143
+ figure covered 18 cases of which 17 were `VisualElement` alone, and no `Button`
144
+ had ever been compared. This one includes a text-heavy working screen, and **ten
145
+ of the sixteen differences are font metrics rather than layout** — both engines
146
+ shrink the same box for the same reason, and only the ruler differs. Three more
147
+ are 1px, two are the `yoga-layout` version difference already known in 0.1.0, and
148
+ **one is genuinely unresolved**. All sixteen are named in
149
+ [`docs/accuracy.en.md`](docs/accuracy.en.md).
150
+
151
+ Colour, borders and corners are held by a CSS-declaration regression baseline
152
+ (`tests/render/visual.test.ts`), which detects our own drift and is not evidence
153
+ of agreement with Unity. That comparison is a one-time eye check, recorded with
154
+ both screenshots in [`docs/visual-check.md`](docs/visual-check.md).
155
+
156
+ ### Known limitations
157
+
158
+ - **Text-driven layout will not match exactly.** A browser cannot measure Unity's
159
+ font asset, so any box sized by its text differs. This is the same fact that
160
+ makes screenshot diffing useless, and it is not fixable here.
161
+ - **A wrapped ScrollView's content height diverges.** Unity clamps it to the
162
+ viewport; Yoga grows it to the content. Unity's rule is not yet identified —
163
+ the fix that matches one case breaks another, so nothing was guessed.
164
+ - Scrollbars reserve space but are not drawn, and there is no scrolling.
165
+ - `-unity-background-scale-mode`'s two non-default values are implemented from
166
+ Unity's documented semantics but have not been seen side by side.
167
+
168
+ ## 0.1.0 — 2026-08-03
4
169
 
5
170
  First release. Parses Unity UI Toolkit `.uxml` and `.uss`, lays them out through
6
171
  the same Yoga engine UI Toolkit uses, paints to the DOM, and writes the files
package/README.md CHANGED
@@ -21,8 +21,19 @@ stylesheet comes out wrong.</sub>
21
21
 
22
22
  [English](#english) · [한국어](#한국어)
23
23
 
24
- > ⚠️ **Status: pre-alpha.** Under active development. Not yet published to npm.
25
- > 현재 개발 초기 단계입니다. 아직 npm에 배포되지 않았습니다.
24
+ ```bash
25
+ npm install uxml-preview
26
+ ```
27
+
28
+ > **v0.2.0 — early release.** Five controls (`VisualElement`, `Label`, `Button`,
29
+ > `Image`, `ScrollView`), everything else drawn as a plain box with a warning,
30
+ > and a support matrix that says plainly what has not been measured.
31
+ > Read [what is not verified yet](#what-is-not-verified-yet) before relying on it.
32
+ >
33
+ > **v0.2.0 — 초기 릴리스입니다.** 컨트롤 5종(`VisualElement`, `Label`, `Button`,
34
+ > `Image`, `ScrollView`)이고, 나머지는 경고와 함께 일반 박스로 그립니다.
35
+ > 검증되지 않은 부분은 지원 범위표에 그대로 적어뒀습니다. 쓰시기 전에
36
+ > [아직 검증되지 않은 것](#아직-검증되지-않은-것)을 먼저 봐주세요.
26
37
 
27
38
  ---
28
39
 
@@ -36,8 +47,10 @@ directly in a web browser. It uses **Yoga** — the same layout engine Unity UI
36
47
  itself uses — compiled to WebAssembly, so results match the Unity Editor rather than
37
48
  approximating it.
38
49
 
39
- Measured against Unity 6000.0.40f1: **242 of 244 element coordinates identical**
40
- across 18 layout cases. The two that differ are named and explained in
50
+ Measured against Unity 6000.0.40f1: **548 of 564 element coordinates identical**
51
+ across 33 layout cases, including a full working screen. Ten of the sixteen
52
+ differences are font metrics rather than layout — a browser cannot measure
53
+ Unity's font asset. All sixteen are named in
41
54
  [`docs/accuracy.en.md`](docs/accuracy.en.md).
42
55
 
43
56
  ```
@@ -84,8 +97,9 @@ That broken feedback loop is what this library fixes.
84
97
  | 3 | USS style resolver | ✅ |
85
98
  | 4 | Yoga layout + DOM painting | ✅ |
86
99
  | 5 | Golden tests against Unity | ✅ |
87
- | 6 | **v0.1 release** | 🔨 |
88
- | 7+ | More controls, editing layer, tool integration | ⬜ |
100
+ | 6 | **v0.1 release** | ✅ |
101
+ | 7 | **v0.2 — ScrollView, Image, states, a measured screen** | ✅ |
102
+ | 8+ | Editing layer, tool integration | ⬜ |
89
103
 
90
104
  Full plan: [`docs/ROADMAP.md`](docs/ROADMAP.md)
91
105
 
@@ -94,16 +108,25 @@ Full plan: [`docs/ROADMAP.md`](docs/ROADMAP.md)
94
108
  The support matrix is deliberately conservative — see
95
109
  [`docs/supported.en.md`](docs/supported.en.md) for the full table.
96
110
 
97
- - **Only geometry was compared, and only as far as Yoga.** The figure above is
98
- measured at the coordinates the layout engine produces. Whether the painted
99
- DOM reproduces them is checked by an invariant of our own rather than against
100
- Unity. Colours, borders, corner radii and fonts are implemented but were not
101
- measured at all; coordinates say nothing about them.
111
+ - **Only geometry is compared automatically, and only as far as Yoga.** The
112
+ figure above is measured at the coordinates the layout engine produces.
113
+ Whether the painted DOM reproduces them is checked by an invariant of our own
114
+ rather than against Unity.
115
+ - **Colour, borders and corners rest on a single eye comparison.** One screen was
116
+ put side by side with Unity and checked item by item
117
+ ([`docs/visual-check.md`](docs/visual-check.md)); it found two real defects, so
118
+ it was worth doing, but it is one screen and not an automated diff. Everything
119
+ it did not contain is still unmeasured.
102
120
  - **Text measurement was not compared.** Unity renders text with its own font
103
- asset. Layout that is driven by measured text will not match exactly, and the
104
- line-height factor is still an estimate.
105
- - **`Button` has no golden case.** It renders through the same path as `Label`,
106
- but "same path" is not a measurement.
121
+ asset, so any box sized by its text differs — this accounts for ten of the
122
+ sixteen coordinate divergences. The line-height factor is still an estimate.
123
+ - **A wrapped `ScrollView`'s content height diverges.** Unity clamps it to the
124
+ viewport; Yoga grows it to the content. Unity's rule is not yet identified.
125
+ - **Nested disabled elements dim twice.** `enabled="false"` disables a subtree
126
+ and each level takes the half opacity, so a disabled control inside a disabled
127
+ panel comes out at a quarter. Unity almost certainly dims once; this is not
128
+ measured yet.
129
+ - Scrollbars reserve their width but are not drawn, and nothing scrolls.
107
130
  - `-unity-slice-*`, `-unity-background-image-tint-color` and `transition` are
108
131
  parsed and preserved, but not drawn.
109
132
 
@@ -121,6 +144,10 @@ const doc = parse(uxmlText, ussText);
121
144
  // model → screen
122
145
  const view = render(doc, document.getElementById('preview'), {
123
146
  resolveAsset: (path) => myAssetUrlFor(path),
147
+
148
+ // Pseudo-class states are explicit input, keyed by USS selector, so the same
149
+ // call always draws the same picture. Per element, because real screens mix.
150
+ states: { '#UseButton': ['hover'], '#DropButton': ['disabled'] },
124
151
  });
125
152
  console.log(view.warnings);
126
153
 
@@ -183,18 +210,21 @@ Checked by comparing element geometry against the Unity Editor, case by case.
183
210
  Geometry rather than pixels: Unity draws text with its own font asset and a
184
211
  browser does not, so a pixel diff would measure the font more than the layout.
185
212
 
186
- Measured against Unity on 2026-08-02, over 18 cases and 61 elements:
213
+ Measured against Unity on 2026-08-06, over 33 cases and 141 elements:
187
214
 
188
215
  | | |
189
216
  |---|---|
190
- | Cases matching exactly | **17 / 18** |
191
- | Values within 0.5px | **242 / 244 (99.2%)** |
192
-
193
- The one divergence is a percentage sized against a parent that has no explicit
194
- size, where `yoga-layout` resolves the main axis and the Yoga inside UI Toolkit
195
- does not. It is recorded rather than smoothed over — see
196
- [`docs/accuracy.en.md`](docs/accuracy.en.md) for the numbers, the failed attempts to
197
- configure around it, and how to reproduce the measurement.
217
+ | Cases matching exactly | **31 / 33** |
218
+ | Values within 0.5px | **548 / 564 (97.2%)** |
219
+
220
+ The sixteen divergences are four different things, and the distinction matters
221
+ more than the ratio: **ten are text metrics**, three are 1px, two are a
222
+ `yoga-layout` version difference, and **one is unresolved**. The ten are not a
223
+ layout defect — both engines shrink the same box for the same reason and only
224
+ the ruler differs, which is the same fact that makes pixel diffing useless.
225
+ Every one is named in [`docs/accuracy.en.md`](docs/accuracy.en.md), along with
226
+ the failed attempts to configure around the version difference and how to
227
+ reproduce the measurement.
198
228
 
199
229
  ### Questions this answers
200
230
 
@@ -216,10 +246,11 @@ as nested flex, and computed values have to be resolved to fixed numbers or
216
246
  calculated in C#.
217
247
 
218
248
  **How close is the output to Unity?**
219
- 242 of 244 element coordinates identical against Unity 6000.0.40f1, across 18
220
- layout cases. Geometry is compared rather than screenshots, because Unity draws
221
- text with its own font asset. The single divergence and the numbers behind this
222
- are in [`docs/accuracy.en.md`](docs/accuracy.en.md).
249
+ 548 of 564 element coordinates identical against Unity 6000.0.40f1, across 33
250
+ layout cases including a complete inventory screen. Geometry is compared rather
251
+ than screenshots, because Unity draws text with its own font asset — which is
252
+ also why ten of the sixteen differences are font metrics rather than layout.
253
+ Every divergence is named in [`docs/accuracy.en.md`](docs/accuracy.en.md).
223
254
 
224
255
  **Will it damage my files if I edit them?**
225
256
  Editing goes through a model that keeps the original text. An unedited document
@@ -228,9 +259,10 @@ CRLF endings included — and an edit rewrites only the region it touched. The
228
259
  playground shows this live as `round-trip: exact`.
229
260
 
230
261
  **What happens to controls it does not support?**
231
- They are kept in the tree, reported as a warning, excluded from rendering, and
232
- written back out unchanged. One unsupported control never takes down the rest of
233
- the screen, and nothing is silently dropped.
262
+ They are drawn as a plain `VisualElement`, reported as a warning, and written
263
+ back out unchanged. Their own styles and their children come out normally; only
264
+ what makes that control look like itself is missing. One unfamiliar tag never
265
+ takes down the rest of the screen, and nothing is silently dropped.
234
266
 
235
267
  ### License
236
268
 
@@ -248,9 +280,10 @@ Apache-2.0
248
280
  **Yoga**를 WebAssembly로 그대로 사용하기 때문에, 결과를 "비슷하게 흉내내는" 것이 아니라
249
281
  유니티 에디터와 일치시킵니다.
250
282
 
251
- Unity 6000.0.40f1과 대조한 결과, 레이아웃 케이스 18개에서
252
- **요소 좌표 244개 중 242개가 완전히 일치**합니다. 어긋난 2개는
253
- [`docs/accuracy.md`](docs/accuracy.md)에 사유까지 적어뒀습니다.
283
+ Unity 6000.0.40f1과 대조한 결과, 실무형 화면을 포함한 레이아웃 케이스 33개에서
284
+ **요소 좌표 564개 중 548개가 일치**합니다. 어긋난 16개 중 **10개는 레이아웃이 아니라
285
+ 폰트 메트릭** 차이입니다 — 브라우저는 유니티 폰트 에셋을 잴 수 없습니다.
286
+ 16개 전부 [`docs/accuracy.md`](docs/accuracy.md)에 이름과 사유를 적어뒀습니다.
254
287
 
255
288
  ```
256
289
  .uxml + .uss → 파싱 → 스타일 계산 → Yoga 레이아웃 → DOM 페인팅
@@ -295,8 +328,9 @@ Unity 6000.0.40f1과 대조한 결과, 레이아웃 케이스 18개에서
295
328
  | 3 | USS 스타일 리졸버 | ✅ |
296
329
  | 4 | Yoga 레이아웃 + DOM 페인팅 | ✅ |
297
330
  | 5 | 유니티 대조 골든 테스트 | ✅ |
298
- | 6 | **v0.1 공개** | 🔨 |
299
- | 7+ | 컨트롤 확장, 편집 레이어, 도구 통합 | ⬜ |
331
+ | 6 | **v0.1 공개** | ✅ |
332
+ | 7 | **v0.2 — ScrollView·Image·상태, 대표 화면 대조** | ✅ |
333
+ | 8+ | 편집 레이어, 도구 통합 | ⬜ |
300
334
 
301
335
  전체 계획: [`docs/ROADMAP.md`](docs/ROADMAP.md)
302
336
 
@@ -305,15 +339,22 @@ Unity 6000.0.40f1과 대조한 결과, 레이아웃 케이스 18개에서
305
339
  지원 범위는 일부러 보수적으로 적었습니다. 전체 표는
306
340
  [`docs/supported.md`](docs/supported.md)에 있습니다.
307
341
 
308
- - **좌표만, 그것도 Yoga 층까지만 비교했습니다.** 위 수치는 레이아웃 엔진이 낸
342
+ - **자동 대조는 좌표만, 그것도 Yoga 층까지입니다.** 위 수치는 레이아웃 엔진이 낸
309
343
  좌표를 잰 것입니다. 그려진 DOM이 그 좌표를 재현하는지는 유니티가 아니라 자체
310
- 불변식 검사로 봅니다. 색·테두리·모서리·폰트는 구현돼 있지만 아예 측정하지
311
- 않았습니다 — 좌표로는 알 수 없는 것들입니다
344
+ 불변식 검사로 봅니다
345
+ - **색·테두리·모서리는 육안 대조 1회에 기대고 있습니다.** 화면 하나를 유니티와
346
+ 나란히 놓고 항목별로 확인했고([`docs/visual-check.md`](docs/visual-check.md)),
347
+ 거기서 실제 결함 두 개가 나왔으니 할 값어치는 있었습니다. 다만 **화면 하나이고
348
+ 자동 diff가 아닙니다.** 그 화면에 없던 것은 여전히 미측정입니다
312
349
  - **텍스트 측정은 대조하지 않았습니다.** 유니티는 자기 폰트 에셋으로 글자를
313
- 그립니다. 측정된 텍스트가 좌우하는 레이아웃은 정확히 맞지 않고, 행 높이
314
- 계수도 아직 추정치입니다
315
- - **`Button`은 골든 케이스가 없습니다.** `Label`과 같은 경로로 그려지지만,
316
- "같은 경로"는 측정이 아닙니다
350
+ 그리므로 텍스트가 크기를 좌우하는 박스는 어긋납니다 — 좌표 불일치 16건 중
351
+ 10건이 이것입니다. 행 높이 계수도 아직 추정치입니다
352
+ - **wrap된 `ScrollView`의 콘텐츠 높이가 어긋납니다.** 유니티는 뷰포트에 묶고
353
+ Yoga는 콘텐츠로 키웁니다. 유니티의 규칙을 아직 특정하지 못했습니다
354
+ - **중첩된 비활성 요소가 두 번 흐려집니다.** `enabled="false"`가 하위 트리를
355
+ 비활성으로 만들고 각 층이 0.5 불투명도를 받아 1/4이 됩니다. 유니티는 한 번만
356
+ 흐리게 할 것이 거의 확실하지만 아직 측정하지 않았습니다
357
+ - 스크롤바는 폭만 예약하고 그리지 않으며, 스크롤되지 않습니다
317
358
  - `-unity-slice-*`, `-unity-background-image-tint-color`, `transition`은
318
359
  파싱·보존은 되지만 그려지지 않습니다
319
360
 
@@ -340,7 +381,7 @@ pnpm dev
340
381
 
341
382
  예제는 예쁜 것보다 **함정을 보여주는 것**으로 골랐습니다 — `flex-direction`의 기본값이
342
383
  `column`이라는 것, 겹침 순서가 `z-index`가 아니라 마크업 순서로 정해진다는 것,
343
- 미지원 컨트롤이 그려지지는 않지만 경고와 함께 왕복에서 살아남는다는 것.
384
+ 전용 렌더러가 없는 컨트롤도 일반 박스로 그려지고 경고와 함께 왕복에서 살아남는다는 것.
344
385
 
345
386
  ### 개발
346
387
 
@@ -373,10 +414,11 @@ pnpm build
373
414
  계산이 필요한 값은 고정값으로 풀거나 C#에서 계산해야 합니다.
374
415
 
375
416
  **유니티와 얼마나 같나요?**
376
- Unity 6000.0.40f1 기준, 레이아웃 케이스 18개에서 **요소 좌표 244개 중 242개 일치**
377
- 입니다. 스크린샷이 아니라 좌표를 비교하는데, 유니티가 자기 폰트 에셋으로 글자를
378
- 그리기 때문입니다. 유일한 불일치와 근거 수치는
379
- [`docs/accuracy.md`](docs/accuracy.md)에 있습니다.
417
+ Unity 6000.0.40f1 기준, 인벤토리 화면 전체를 포함한 레이아웃 케이스 33개에서
418
+ **요소 좌표 564개 중 548개 일치**입니다. 스크린샷이 아니라 좌표를 비교하는데,
419
+ 유니티가 자기 폰트 에셋으로 글자를 그리기 때문입니다 — 그리고 그 사실 때문에
420
+ 불일치 16개 중 **10개가 레이아웃이 아니라 폰트 메트릭**입니다. 전부
421
+ [`docs/accuracy.md`](docs/accuracy.md)에 이름이 적혀 있습니다.
380
422
 
381
423
  **편집하면 파일이 망가지지 않나요?**
382
424
  편집은 원본 텍스트를 그대로 들고 있는 모델을 거칩니다. 고치지 않은 문서는 주석·속성
@@ -384,9 +426,9 @@ Unity 6000.0.40f1 기준, 레이아웃 케이스 18개에서 **요소 좌표 244
384
426
  영역만 바뀝니다. 놀이터의 `round-trip: exact` 표시가 이걸 실시간으로 보여줍니다.
385
427
 
386
428
  **지원하지 않는 컨트롤은 어떻게 되나요?**
387
- 트리에 유지되고, 경고로 보고되고, 렌더링에서만 빠지고, 저장하면 그대로 다시
388
- 나옵니다. 미지원 컨트롤 하나 때문에 화면 전체가 죽지 않고, 조용히 사라지는 것도
389
- 없습니다.
429
+ 일반 `VisualElement`처럼 그려지고, 경고로 보고되고, 저장하면 그대로 다시 나옵니다.
430
+ 자기 스타일과 자식은 정상적으로 나오며 그 컨트롤 고유의 모양만 빠집니다. 모르는 태그
431
+ 하나 때문에 화면 전체가 죽지 않고, 조용히 사라지는 것도 없습니다.
390
432
 
391
433
  ### 라이선스
392
434
 
@@ -1,10 +1,67 @@
1
1
  import { ElementNode } from '../model/types';
2
+ /**
3
+ * An element a control builds for itself, which the file never mentions.
4
+ *
5
+ * Unity's ScrollView is one tag that becomes four elements, and the three it
6
+ * adds are what decide where everything inside lands — a child of a ScrollView
7
+ * is not a child of the ScrollView. Reproducing the box without them puts every
8
+ * descendant in the wrong place, so they are modelled rather than approximated.
9
+ *
10
+ * `name` is Unity's own, and it is the key the layout dump joins on: these are
11
+ * observed in `tests/golden/unity/scrollview-*.json`, not invented.
12
+ */
13
+ export interface ControlPart {
14
+ /** Unity's name, e.g. `unity-content-viewport`. */
15
+ name: string;
16
+ /**
17
+ * USS this part always carries, because the control fixes it.
18
+ *
19
+ * Written as USS rather than as CSS or Yoga calls so it goes through the same
20
+ * cascade output, Yoga mapping and CSS mapping as everything else. A second
21
+ * styling path for parts would be a second set of USS bugs.
22
+ */
23
+ style: Readonly<Record<string, string>>;
24
+ }
2
25
  export interface ControlSpec {
3
- /** Renders the `text` attribute as its content. */
26
+ /** Renders the `text` attribute as its own content. */
4
27
  hasText: boolean;
28
+ /**
29
+ * Elements the control inserts between itself and the file's children,
30
+ * outermost first. The file's children go inside the last one.
31
+ */
32
+ parts: readonly ControlPart[];
33
+ /**
34
+ * USS classes Unity's own constructor puts on this control, which the theme
35
+ * stylesheet then targets. Not in the `class` attribute and not in the model:
36
+ * the file says `<ui:Button />` and Unity adds the rest.
37
+ */
38
+ classes: readonly string[];
39
+ }
40
+ export interface ResolvedControl {
41
+ spec: ControlSpec;
42
+ /**
43
+ * No renderer of its own; drawn as a plain `VisualElement`. Callers report
44
+ * this once per element so the preview says what it did (CLAUDE.md rule 6).
45
+ */
46
+ fallback: boolean;
5
47
  }
6
48
  /** The document container. Not a control, but it is the panel's root box. */
7
49
  export declare const ROOT_LOCAL_NAME = "UXML";
8
- export declare function controlFor(node: ElementNode): ControlSpec | null;
50
+ export declare function isNonVisual(node: ElementNode): boolean;
51
+ /**
52
+ * Purpose: the spec to draw `node` with, always.
53
+ * Ensures: never null. `fallback` is true only for elements a caller
54
+ * should report; the root is excluded because it is the panel
55
+ * box rather than a control that failed to resolve.
56
+ */
57
+ export declare function resolveControl(node: ElementNode): ResolvedControl;
9
58
  export declare function isRoot(node: ElementNode): boolean;
59
+ /**
60
+ * Purpose: the USS classes Unity would have added to this element.
61
+ * Ensures: empty for anything with no renderer of its own — a fallback box
62
+ * must not pick up theme styling meant for a control we do not
63
+ * actually draw.
64
+ */
65
+ export declare function implicitClassesOf(node: ElementNode): readonly string[];
66
+ /** Controls with a renderer of their own. Everything else still draws. */
10
67
  export declare function supportedControlNames(): string[];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Unity's control defaults, as far as we have measured them.
3
+ *
4
+ * Unity ships a theme stylesheet that gives controls their own margin, padding
5
+ * and border before any author USS runs. This renderer supplied none of it, so
6
+ * every Button sat 3px left and 1px high of where Unity puts it — invisible on
7
+ * one button and compounding across a row.
8
+ *
9
+ * **Only measured values live here.** Every declaration below was derived from
10
+ * a Unity layout dump in tests/golden/unity/, and adding `Button`'s margin
11
+ * moved all five Button cases from 18 mismatched values to zero. Values read
12
+ * from Unity's source instead would look identical in this file while being
13
+ * wrong in a way no test could see: they would not appear in the unmeasured
14
+ * list, so they would quietly pass for ground truth.
15
+ *
16
+ * Two kinds of measurement feed this file, and the second was a long time
17
+ * coming. Geometry comes from the coordinate dumps. `-unity-text-align` cannot:
18
+ * where a glyph sits inside a box does not move the box, so no dump can ever
19
+ * see it. It came from the Step 6 eye check — Unity centres a Button's label
20
+ * and this renderer had it at the top left, which is visible the moment the two
21
+ * screenshots sit side by side and invisible in 368 compared numbers.
22
+ *
23
+ * `opacity` on `:disabled` came from the same screenshots, but by sampling
24
+ * pixels rather than looking. Unity's disabled Drop button is rgb(41,50,67);
25
+ * the base colour rgb(58,74,104) composited at half over the footer's
26
+ * rgb(24,26,31) is (41, 50, 67.5) — all three channels exact. That arithmetic
27
+ * is the evidence, and it is why the value is 0.5 and not a number that merely
28
+ * looked about right.
29
+ *
30
+ * Getting there took two wrong turns worth recording. First the author's
31
+ * `Button:disabled` rule was blamed for not applying — it genuinely does not,
32
+ * because `#DropButton` outranks it, in both engines. Then Unity was accused of
33
+ * ranking state rules above specificity, and `state-vs-id` and `state-vs-inline`
34
+ * refuted that: Unity answers 40 to both, exactly as this cascade already did.
35
+ * The dimming was never about the cascade at all.
36
+ *
37
+ * What is deliberately absent, and why:
38
+ *
39
+ * - `Label` — measured, and it has no default margin. `inherit-vs-direct`
40
+ * puts an explicitly-sized Label at the origin of a sized parent, and Unity
41
+ * reports x=0 y=0. Nothing to add is a result, not an omission.
42
+ * - padding, border, font — invisible to the cases we have. Every compared
43
+ * element is explicitly sized, and USS is border-box, so padding and border
44
+ * do not move the outer rectangle. They are unmeasured, so they are not here.
45
+ * - `Label`'s text alignment — the eye check could not separate it from the
46
+ * box at this size. Unmeasured, so absent.
47
+ *
48
+ * The class names were a separate claim from the values, and are now measured
49
+ * too. `unity-button` came from Unity's documented `Button.ussClassName`, which
50
+ * the margin dumps said nothing about — they proved the spacing, not the
51
+ * selector delivering it. The golden case `theme-class-hook` was written to
52
+ * settle exactly that, and Unity answered on 2026-08-05: a `.unity-button` rule
53
+ * in author USS reaches a bare `<ui:Button>`, sizing it 120×40 in both engines.
54
+ * The selector below is therefore measured, not assumed.
55
+ */
56
+ /** Where every value in this file was measured. Theme values move between versions. */
57
+ export declare const THEME_UNITY_VERSION = "6000.0.40f1";
58
+ /**
59
+ * Parsed once and spliced in ahead of author sheets, so an author rule of equal
60
+ * specificity wins on order — which is how a theme stylesheet behaves in Unity.
61
+ *
62
+ * Class selectors rather than type selectors, also for Unity's reasons: a theme
63
+ * rule at `.unity-button` (0,1,0) beats an author's `Button { }` (0,0,1), and
64
+ * that is a real Unity gotcha worth reproducing rather than smoothing over.
65
+ */
66
+ export declare const THEME_USS = ":disabled {\n opacity: 0.5;\n}\n\n.unity-button {\n margin: 1px 3px;\n -unity-text-align: middle-center;\n}\n";
67
+ /**
68
+ * **Unmeasured, and reachable: nested disabled elements.**
69
+ *
70
+ * `enabled="false"` puts a whole subtree into `:disabled` — measured, by
71
+ * `disabled-inherits`. The rule above therefore hands every level its own
72
+ * `opacity: 0.5`, and CSS multiplies them, so a disabled button inside a
73
+ * disabled panel comes out at a quarter. Unity almost certainly dims once.
74
+ *
75
+ * Not guessed at, because the fix has a choice inside it — dim only where
76
+ * `enabled="false"` was written, or suppress the inherited one — and picking
77
+ * without evidence is how the `.unity-button` selector became a hypothesis
78
+ * nobody noticed for a day. Opacity leaves no trace in a coordinate dump, so
79
+ * settling it needs a screenshot of a nested case. Recorded in
80
+ * `docs/backlog.md` and visible in `visual-nested-disabled`.
81
+ */
82
+ /**
83
+ * Purpose: how much width a vertical scrollbar claims, given whether it shows.
84
+ * Ensures: 0 when the content fits — Unity hides the scroller rather than
85
+ * shrinking the viewport for a bar nobody can see.
86
+ */
87
+ export declare function verticalScrollbarWidth(overflows: boolean): number;