@things-factory/figure-ui 10.1.45 → 10.1.47

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.
Files changed (138) hide show
  1. package/client/modeller/angle-dial.ts +70 -0
  2. package/client/modeller/figure-settings.ts +328 -0
  3. package/client/modeller/figure-v3-capability.ts +137 -0
  4. package/client/modeller/figure-v3-clips.ts +253 -0
  5. package/client/modeller/figure-v3-controls.ts +212 -0
  6. package/client/modeller/figure-v3-links.ts +150 -79
  7. package/client/modeller/figure-v3-material.ts +108 -35
  8. package/client/modeller/figure-v3-parts.ts +96 -3
  9. package/client/modeller/figure-v3-preview.ts +326 -0
  10. package/client/modeller/figure-v3-properties.ts +29 -57
  11. package/client/modeller/figure-v3-verdict.ts +598 -60
  12. package/client/modeller/figure-view.ts +152 -0
  13. package/client/modeller/side-panel-styles.ts +326 -0
  14. package/client/modeller/v3-canvas.ts +221 -263
  15. package/client/modeller/v3-clips.ts +89 -0
  16. package/client/modeller/v3-controls.ts +54 -0
  17. package/client/modeller/v3-editor.ts +532 -223
  18. package/client/modeller/v3-fields.ts +62 -0
  19. package/client/modeller/v3-help.ts +41 -0
  20. package/client/modeller/v3-keys.ts +34 -0
  21. package/client/modeller/v3-links.ts +126 -10
  22. package/client/modeller/v3-live.ts +79 -0
  23. package/client/modeller/v3-maturity.ts +415 -0
  24. package/client/modeller/v3-palette.ts +45 -0
  25. package/client/modeller/v3-preview-model.ts +54 -0
  26. package/client/modeller/v3-stage-model.ts +159 -0
  27. package/client/pages/figure-list-page.ts +4 -4
  28. package/client/pages/figure-v3-modeller-page.ts +66 -134
  29. package/dist-client/modeller/angle-dial.d.ts +32 -0
  30. package/dist-client/modeller/angle-dial.js +41 -0
  31. package/dist-client/modeller/angle-dial.js.map +1 -0
  32. package/dist-client/modeller/figure-settings.d.ts +39 -0
  33. package/dist-client/modeller/figure-settings.js +317 -0
  34. package/dist-client/modeller/figure-settings.js.map +1 -0
  35. package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
  36. package/dist-client/modeller/figure-v3-capability.js +141 -0
  37. package/dist-client/modeller/figure-v3-capability.js.map +1 -0
  38. package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
  39. package/dist-client/modeller/figure-v3-clips.js +268 -0
  40. package/dist-client/modeller/figure-v3-clips.js.map +1 -0
  41. package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
  42. package/dist-client/modeller/figure-v3-controls.js +226 -0
  43. package/dist-client/modeller/figure-v3-controls.js.map +1 -0
  44. package/dist-client/modeller/figure-v3-links.d.ts +10 -2
  45. package/dist-client/modeller/figure-v3-links.js +134 -79
  46. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  47. package/dist-client/modeller/figure-v3-material.d.ts +5 -2
  48. package/dist-client/modeller/figure-v3-material.js +109 -35
  49. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  50. package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
  51. package/dist-client/modeller/figure-v3-parts.js +88 -3
  52. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  53. package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
  54. package/dist-client/modeller/figure-v3-preview.js +322 -0
  55. package/dist-client/modeller/figure-v3-preview.js.map +1 -0
  56. package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
  57. package/dist-client/modeller/figure-v3-properties.js +29 -57
  58. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  59. package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
  60. package/dist-client/modeller/figure-v3-verdict.js +579 -59
  61. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  62. package/dist-client/modeller/figure-view.d.ts +118 -0
  63. package/dist-client/modeller/figure-view.js +78 -0
  64. package/dist-client/modeller/figure-view.js.map +1 -0
  65. package/dist-client/modeller/side-panel-styles.d.ts +23 -0
  66. package/dist-client/modeller/side-panel-styles.js +326 -0
  67. package/dist-client/modeller/side-panel-styles.js.map +1 -0
  68. package/dist-client/modeller/v3-canvas.d.ts +29 -37
  69. package/dist-client/modeller/v3-canvas.js +188 -260
  70. package/dist-client/modeller/v3-canvas.js.map +1 -1
  71. package/dist-client/modeller/v3-clips.d.ts +39 -0
  72. package/dist-client/modeller/v3-clips.js +58 -0
  73. package/dist-client/modeller/v3-clips.js.map +1 -0
  74. package/dist-client/modeller/v3-controls.d.ts +28 -0
  75. package/dist-client/modeller/v3-controls.js +28 -0
  76. package/dist-client/modeller/v3-controls.js.map +1 -0
  77. package/dist-client/modeller/v3-editor.d.ts +58 -1
  78. package/dist-client/modeller/v3-editor.js +556 -218
  79. package/dist-client/modeller/v3-editor.js.map +1 -1
  80. package/dist-client/modeller/v3-fields.d.ts +30 -0
  81. package/dist-client/modeller/v3-fields.js +46 -0
  82. package/dist-client/modeller/v3-fields.js.map +1 -0
  83. package/dist-client/modeller/v3-help.d.ts +18 -0
  84. package/dist-client/modeller/v3-help.js +37 -0
  85. package/dist-client/modeller/v3-help.js.map +1 -0
  86. package/dist-client/modeller/v3-keys.d.ts +14 -0
  87. package/dist-client/modeller/v3-keys.js +23 -0
  88. package/dist-client/modeller/v3-keys.js.map +1 -0
  89. package/dist-client/modeller/v3-links.d.ts +44 -2
  90. package/dist-client/modeller/v3-links.js +125 -8
  91. package/dist-client/modeller/v3-links.js.map +1 -1
  92. package/dist-client/modeller/v3-live.d.ts +31 -0
  93. package/dist-client/modeller/v3-live.js +58 -0
  94. package/dist-client/modeller/v3-live.js.map +1 -0
  95. package/dist-client/modeller/v3-maturity.d.ts +120 -0
  96. package/dist-client/modeller/v3-maturity.js +317 -0
  97. package/dist-client/modeller/v3-maturity.js.map +1 -0
  98. package/dist-client/modeller/v3-palette.d.ts +7 -0
  99. package/dist-client/modeller/v3-palette.js +42 -0
  100. package/dist-client/modeller/v3-palette.js.map +1 -0
  101. package/dist-client/modeller/v3-preview-model.d.ts +32 -0
  102. package/dist-client/modeller/v3-preview-model.js +48 -0
  103. package/dist-client/modeller/v3-preview-model.js.map +1 -0
  104. package/dist-client/modeller/v3-stage-model.d.ts +31 -0
  105. package/dist-client/modeller/v3-stage-model.js +121 -0
  106. package/dist-client/modeller/v3-stage-model.js.map +1 -0
  107. package/dist-client/pages/figure-list-page.js +4 -4
  108. package/dist-client/pages/figure-list-page.js.map +1 -1
  109. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
  110. package/dist-client/pages/figure-v3-modeller-page.js +64 -136
  111. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  112. package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
  113. package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -0
  114. package/dist-client/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +6 -6
  116. package/test/v3-clips.test.ts +70 -0
  117. package/test/v3-controls.test.ts +68 -0
  118. package/test/v3-fields.test.ts +62 -0
  119. package/test/v3-gizmo.test.ts +3 -5
  120. package/test/v3-keys.test.ts +37 -0
  121. package/test/v3-layout.test.ts +78 -0
  122. package/test/v3-links.test.ts +118 -7
  123. package/test/v3-live.test.ts +99 -0
  124. package/test/v3-material.test.ts +6 -0
  125. package/test/v3-maturity.test.ts +268 -0
  126. package/test/v3-parts-wiring.test.ts +3 -1
  127. package/test/v3-preview.test.ts +42 -0
  128. package/test/v3-properties.test.ts +4 -2
  129. package/test/v3-scene-canvas.test.ts +85 -0
  130. package/test/v3-stage-model.test.ts +25 -0
  131. package/test/v3-theme.test.ts +16 -25
  132. package/test/v3-verdict.test.ts +12 -6
  133. package/test/v3-view.test.ts +20 -0
  134. package/translations/en.json +98 -5
  135. package/translations/ja.json +100 -7
  136. package/translations/ko.json +98 -5
  137. package/translations/ms.json +100 -7
  138. package/translations/zh.json +100 -7
@@ -0,0 +1,99 @@
1
+ /*
2
+ * **끄는 동안 자연스럽게** (사용자 2026-09-24: 「조작값도 인풋값이 변화하는 과정도 모두 이벤트처리해서 편집기에서도
3
+ * 자연스럽게 움직이면 좋겠다」). 끄는 중간값은 그 자세로 바로 그리고 아무것도 기록하지 않는다; 놓을 때 한 번 기록한다.
4
+ */
5
+ import { test } from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { readFileSync } from 'node:fs'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ import { addV3Box, applyV3Authoring, createV3Asset } from '@hatiolab/figure-model'
11
+
12
+ import { liveOverridesOf, livePartsOf, NOTHING_LIVE, scrubbed, scrubStep } from '../client/modeller/v3-live.ts'
13
+
14
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
15
+
16
+ const slider = () => {
17
+ let asset = createV3Asset('Slider')
18
+ asset = addV3Box(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
19
+ return applyV3Authoring(asset, { kind: 'add-motion', part: 'block', motion: { kind: 'slide', axis: 'x' }, state: { id: 'shift', unit: 'mm', min: 0, max: 60, start: 0 } })
20
+ }
21
+
22
+ test('끄는 값으로 그 자세를 바로 그린다 — 도형은 그대로', () => {
23
+ const asset = slider()
24
+ const before = JSON.stringify(asset)
25
+ const parts = livePartsOf(asset, { values: { shift: 45 } })!
26
+ assert.equal(Math.round(parts.find(p => p.id === 'block')!.pose.t[0]!), 45)
27
+ assert.equal(JSON.stringify(asset), before, '아무것도 기록하지 않는다')
28
+ assert.equal(livePartsOf(asset, NOTHING_LIVE), null, '끄는 것이 없으면 평소대로')
29
+ })
30
+
31
+ test('속성창의 설계 값도 치는 동안 그린다 — 크기가 따라 변하고 도형은 그대로', () => {
32
+ const asset = slider()
33
+ const before = JSON.stringify(asset)
34
+ const size = (parts: { id: string; shape?: unknown }[]) => JSON.stringify((parts.find(p => p.id === 'block') as { shape: unknown }).shape)
35
+ const now = livePartsOf(asset, { values: { 'block.size.x': 100 } })!
36
+ const wider = livePartsOf(asset, { values: { 'block.size.x': 300 } })!
37
+ assert.notEqual(size(wider), size(now), '크기 300 이 바로 그려진다')
38
+ assert.equal(JSON.stringify(asset), before, '기록은 놓을 때')
39
+ })
40
+
41
+ test('설계 값과 자세를 가른다 — 모르는 이름은 버린다', () => {
42
+ assert.deepEqual(liveOverridesOf(slider(), { shift: 10, 'block.size.x': 5, nothing: 1 }), { design: { 'block.size.x': 5 }, state: { shift: 10 } })
43
+ })
44
+
45
+ test('치는 도중 도형이 못 받는 값이면 지금 모습을 그린다 — 지어낸 도형을 보이지 않는다', () => {
46
+ assert.equal(livePartsOf(slider(), { values: { 'block.size.x': -5 } }), null)
47
+ })
48
+
49
+ test('재질의 끄는 값(발광)도 그린다 — 편집 하나를 기록 없이 적용해서', () => {
50
+ const asset = slider()
51
+ const glowing = livePartsOf(asset, {
52
+ values: {},
53
+ edit: { kind: 'set-material', target: 'block', material: { token: 'palette.primary', emissive: { token: 'palette.primary', intensity: 0.5, on: true } } }
54
+ })!
55
+ assert.equal(glowing.find(p => p.id === 'block')!.material.emissive?.intensity, 0.5)
56
+ assert.equal(asset.appearance[0]!.material.emissive, undefined, '도형은 그대로')
57
+ })
58
+
59
+ test('이름표를 끌면 값이 크기에 맞는 걸음으로 움직인다 — 각도 1°, 길이는 자릿수에 따라, Shift 는 열 배 곱게', () => {
60
+ assert.equal(scrubStep('deg', 30), 1)
61
+ assert.equal(scrubStep('mm', 50), 1)
62
+ assert.equal(scrubStep('mm', 12000), 100)
63
+ assert.equal(scrubStep('mm', 1200), 10)
64
+ assert.equal(scrubStep('mm', 1200, true), 1)
65
+ assert.equal(scrubStep('ratio', 0.4), 0.01)
66
+ assert.equal(scrubbed(1200, 7, 10), 1270)
67
+ assert.equal(scrubbed(0.4, 3, 0.01), 0.43, '0.43000000000000005 가 아니다')
68
+ })
69
+
70
+ test('속성창 숫자 칸은 치는 동안 알리고, 이름표를 끌 수 있다 — 기록은 change 에서', () => {
71
+ const editor = read('../client/modeller/v3-editor.ts')
72
+ assert.ok(editor.includes('@input=${(e: Event) => this.typing(group.cells[axis] as never, e)}'))
73
+ assert.ok(editor.includes('@input=${(e: Event) => this.typing(field as never, e)}'))
74
+ assert.ok(editor.includes('@pointerdown=${(e: PointerEvent) => this.scrub(group.cells[axis] as never, e, unavailable)}'))
75
+ for (const event of ['@v3-state-live=', '@v3-states-live=', '@v3-part-live=']) assert.ok(editor.includes(event), event)
76
+ assert.ok(editor.includes('livePartsOf(asset as V3Asset, this.live)') && editor.includes('.parts=${this.drawnParts(view.geometry, asset)}'), '캔버스가 끄는 값을 그린다')
77
+ const run = editor.slice(editor.indexOf(' private run(action'), editor.indexOf(' private hold('))
78
+ assert.ok(run.includes('this.live = NOTHING_LIVE'), '기록하거나 거절되면 끄는 값을 버린다')
79
+ const material = read('../client/modeller/figure-v3-material.ts')
80
+ assert.ok(material.includes('@input=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value), true)}'))
81
+ })
82
+
83
+ test('슬라이더와 클립 막대는 끄는 동안 알리고, 놓을 때 한 번 기록한다', () => {
84
+ const controls = read('../client/modeller/figure-v3-controls.ts')
85
+ assert.ok(controls.includes('@input=${(e: Event) => this.drag(c, number(e))}') && controls.includes('@change=${(e: Event) => this.try(c, number(e))}'))
86
+ assert.ok(controls.includes("new CustomEvent('v3-state-live'"))
87
+ const clips = read('../client/modeller/figure-v3-clips.ts')
88
+ assert.ok(clips.includes('@input=${(e: Event) => this.scrub(clip, number(e), true)}'))
89
+ assert.ok(clips.includes("new CustomEvent('v3-states-live'"))
90
+ })
91
+
92
+ test('클립 트랙의 머리는 이름 한 줄, 그 아래 반복 방식과 지우기 — 이름이 한 글자씩 쌓이지 않는다', () => {
93
+ const clips = read('../client/modeller/figure-v3-clips.ts')
94
+ const head = clips.slice(clips.indexOf(' div[track-head] {'), clips.indexOf(' div[keys] {'))
95
+ assert.ok(head.includes('display: grid;') && !head.includes('display: flex;'), '공통 스타일의 폭 100% 가 이름을 누르지 않는다')
96
+ assert.ok(head.includes('grid-column: 1 / -1;'), '이름은 제 줄')
97
+ assert.ok(head.includes('width: 30px;'), '지우기는 작은 버튼')
98
+ })
99
+
@@ -66,3 +66,9 @@ test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 도형
66
66
  assert.match(material, /this\.palette\[token\] === undefined && offered\[token\] !== undefined/)
67
67
  assert.match(material, /\.\.\.\(Object\.keys\(palette\)\.length \? \{ palette \} : \{\}\)/)
68
68
  })
69
+
70
+ test('분할 수는 V2 처럼 — 흔한 넷은 다각형 윤곽과 삼각형 수로, 나머지는 접어 둔다', () => {
71
+ const source = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-material.ts', import.meta.url)), 'utf8')
72
+ assert.ok(source.includes('SEGMENT_PRESETS.slice(0, 4).map(') && source.includes('${silhouette(preset)}'))
73
+ assert.ok(source.includes('<details free') && source.includes('SEGMENT_PRESETS.slice(4)'))
74
+ })
@@ -0,0 +1,268 @@
1
+ /*
2
+ * **V3 성숙도 탭은 V2 리포트를 V3 수로 다시 보인다** (사용자 2026-09-24: 「분석부분도 많이 부족하고, 왜 이렇게 너프되었는지」).
3
+ *
4
+ * V2 리포트(figure-report.ts)는 개수 · 등급과 눈금 · 등급을 정한 한도 · 여섯 축 radar · 한도 막대 · 그리는 비용 · 재사용
5
+ * 추이를 보였다. 여기서는 그 수들이 V3 자산에서 V2 의 셈 그대로 나오는지, V3 렌더러가 실제로 나눠 쓰는 것을 세는지,
6
+ * 화면이 그 순서와 말로 그리는지를 본다.
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { readFileSync } from 'node:fs'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ import { addV3Box, addV3Primitive, applyV3Authoring, compileV3Asset, createV3Asset, LIMITS, v3ScoreOf } from '@hatiolab/figure-model'
14
+ import type { V3Asset } from '@hatiolab/figure-model'
15
+ import { acquireV3Geometry, releaseV3Geometry } from '@hatiolab/things-scene'
16
+
17
+ import {
18
+ BOARD_ASSETS,
19
+ budgetOf,
20
+ curveLayout,
21
+ decadeSteps,
22
+ GRADE_SCALE,
23
+ gradeOf,
24
+ V3_BUDGET_KEYS,
25
+ v3ReportOf
26
+ } from '../client/modeller/v3-maturity.ts'
27
+ import { capacityFrom } from '../client/modeller/scene-budget.ts'
28
+
29
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
30
+ const verdict = read('../client/modeller/figure-v3-verdict.ts')
31
+ const maturity = read('../client/modeller/v3-maturity.ts')
32
+
33
+ /** An asset with the given tokens in its palette, so boxes can differ in colour. */
34
+ function withPalette(...tokens: string[]): V3Asset {
35
+ const asset = createV3Asset('Probe')
36
+ for (const token of tokens) asset.palette[token] = '#336699'
37
+ return asset
38
+ }
39
+
40
+ const cube = (id: string, x: number, token = 'palette.primary', extra: Record<string, unknown> = {}) => ({
41
+ id,
42
+ size: { x: 100, y: 100, z: 100 },
43
+ position: { x, y: 50, z: 0 },
44
+ material: { token, ...extra }
45
+ })
46
+
47
+ test('부품이 없으면 리포트가 없다 — 0 으로 그리지 않는다', () => {
48
+ assert.equal(v3ReportOf(createV3Asset('Empty')), undefined)
49
+ })
50
+
51
+ test('등급은 V2 의 눈금이다 — 한도의 절반 A · 4분의 3 B · 한도 C · 넘치면 D', () => {
52
+ assert.deepEqual(GRADE_SCALE, [
53
+ ['A', 0.5],
54
+ ['B', 0.75],
55
+ ['C', 1]
56
+ ])
57
+ assert.equal(gradeOf(0.5), 'A')
58
+ assert.equal(gradeOf(0.51), 'B')
59
+ assert.equal(gradeOf(0.75), 'B')
60
+ assert.equal(gradeOf(1), 'C')
61
+ assert.equal(gradeOf(1.01), 'D')
62
+ })
63
+
64
+ test('등급은 figure-model 의 v3ScoreOf 그대로다 — 목록과 이 탭이 한 셈으로 매긴다', () => {
65
+ let asset = createV3Asset('Probe')
66
+ asset = addV3Box(asset, cube('a', 0))
67
+ const one = v3ReportOf(asset)!
68
+ assert.deepEqual(one.score, v3ScoreOf(asset))
69
+ assert.deepEqual(V3_BUDGET_KEYS, one.score.budgets.map(b => b.key), '화면이 말을 찾는 키와 셈의 키가 같다')
70
+ })
71
+
72
+ test('draw call 한도 3 은 없다 — 한 화면 166 대 목표에 견준다(수석 아키텍트 판정 09-24)', () => {
73
+ let asset = createV3Asset('Probe')
74
+ for (let i = 0; i < 4; i++) asset = addV3Box(asset, cube(`b${i}`, i * 200))
75
+ const four = v3ReportOf(asset)!
76
+ assert.equal(four.score.grade, 'A', '예전 한도 3 으로는 D 였다')
77
+ const groups = four.score.budgets.find(b => b.key === 'groups')!
78
+ assert.equal(groups.limit, LIMITS.groupsOnScreen / LIMITS.screenInstances)
79
+ assert.ok(four.capacity.instances >= 2 * LIMITS.screenInstances, 'A 는 목표의 두 배가 들어간다는 뜻')
80
+ })
81
+
82
+ test('감춘 부품은 그리지 않으므로 한도에 들지 않는다 — v3CostOf 와 같은 셈', () => {
83
+ let asset = createV3Asset('Probe')
84
+ asset = addV3Box(asset, cube('a', 0))
85
+ asset = addV3Box(asset, cube('b', 200))
86
+ const report = v3ReportOf(asset)!
87
+ assert.equal(report.score.budgets.find(b => b.key === 'groups')!.used, report.cost.groups)
88
+ assert.equal(report.capacity.instances, capacityFrom(report.cost.groups, report.cost.triangles)!.instances)
89
+ })
90
+
91
+ test('투명 재질은 서로 다른 재질로 센다 — 둘이면 한도 1 을 넘고, 그것이 등급을 정한다', () => {
92
+ let asset = withPalette('glass.a', 'glass.b')
93
+ asset = addV3Box(asset, cube('a', 0, 'glass.a', { transparent: true }))
94
+ asset = addV3Box(asset, cube('b', 200, 'glass.b', { transparent: true }))
95
+ const report = v3ReportOf(asset)!
96
+ assert.equal(report.score.budgets.find(b => b.key === 'transparent')!.used, 2)
97
+ assert.equal(report.score.tightest.key, 'transparent')
98
+ assert.equal(report.score.grade, 'D')
99
+
100
+ let same = withPalette('glass.a')
101
+ same = addV3Box(same, cube('a', 0, 'glass.a', { transparent: true }))
102
+ same = addV3Box(same, cube('b', 200, 'glass.a', { transparent: true }))
103
+ assert.equal(v3ReportOf(same)!.score.budgets.find(b => b.key === 'transparent')!.used, 1, '같은 재질은 하나로 나눠 쓴다')
104
+ })
105
+
106
+ test('상태를 따라 움직이는 부품을 센다 — V2 의 「따로 움직이는 부품」', () => {
107
+ let asset = createV3Asset('Probe')
108
+ asset = addV3Box(asset, cube('base', 0))
109
+ asset = addV3Box(asset, cube('arm', 200))
110
+ assert.equal(v3ReportOf(asset)!.score.budgets.find(b => b.key === 'independent')!.used, 0, '전제: 움직임이 없으면 0')
111
+
112
+ asset = applyV3Authoring(asset, {
113
+ kind: 'add-motion',
114
+ part: 'arm',
115
+ motion: { kind: 'turn', axis: 'y' },
116
+ state: { id: 'arm.turn', unit: 'deg', min: 0, max: 90 }
117
+ })
118
+ assert.equal(v3ReportOf(asset)!.score.budgets.find(b => b.key === 'independent')!.used, 1, '도는 팔 하나')
119
+ })
120
+
121
+ test('서로 다른 형상은 things-scene 이 정점 버퍼를 나눠 쓰는 수와 같다', () => {
122
+ let asset = createV3Asset('Probe')
123
+ asset = addV3Box(asset, cube('a', 0))
124
+ asset = addV3Box(asset, cube('b', 200))
125
+ asset = addV3Box(asset, { id: 'c', size: { x: 100, y: 300, z: 100 }, position: { x: 400, y: 150, z: 0 } })
126
+ asset = addV3Primitive(asset, { id: 'd', kind: 'cylinder', dimensions: { radius: 40, length: 100 }, position: { x: 600, y: 50, z: 0 } })
127
+ asset = addV3Primitive(asset, { id: 'e', kind: 'cylinder', dimensions: { radius: 40, length: 100 }, position: { x: 800, y: 50, z: 0 } })
128
+ asset = addV3Primitive(asset, { id: 'f', kind: 'cylinder', dimensions: { radius: 40, length: 100 }, position: { x: 1000, y: 50, z: 0 } })
129
+ // The same cylinder, rounder: another vertex buffer in the renderer, so another shape here.
130
+ asset.appearance.find(look => look.target === 'f')!.segments = 24
131
+ const report = v3ReportOf(asset)!
132
+
133
+ const bodies = compileV3Asset(asset).evaluate().geometry
134
+ const held = bodies.map(body => acquireV3Geometry((body as { shape: never }).shape, (body as { segments?: number }).segments))
135
+ try {
136
+ assert.equal(report.distinctShapes, new Set(held).size, '렌더러의 은행과 같은 키')
137
+ } finally {
138
+ held.forEach(geometry => releaseV3Geometry(geometry))
139
+ }
140
+ assert.equal(report.distinctShapes, 4, '같은 상자 둘 · 키 큰 상자 · 같은 원기둥 둘 · 분할이 다른 원기둥')
141
+ assert.deepEqual(report.reuse, { distinct: 4, parts: 6, saved: 2 })
142
+ assert.equal(report.axes.find(axis => axis.key === 'reuse')!.ratio, 1 - 4 / 6)
143
+ })
144
+
145
+ test('여섯 축 — V2 의 순서, 잴 수 없는 둘은 비율 없이 까닭을 단다', () => {
146
+ let asset = withPalette('palette.accent')
147
+ asset = addV3Box(asset, cube('a', 50))
148
+ asset = addV3Box(asset, cube('b', 150, 'palette.accent'))
149
+ const axes = v3ReportOf(asset)!.axes
150
+
151
+ assert.deepEqual(
152
+ axes.map(axis => axis.key),
153
+ ['scale', 'consistency', 'reuse', 'detail', 'reactive', 'readable']
154
+ )
155
+ const byKey = Object.fromEntries(axes.map(axis => [axis.key, axis]))
156
+ assert.equal(byKey.detail!.ratio, undefined)
157
+ assert.equal(byKey.detail!.blocked, 'figure.text.maturity-blocked-no-detail-level', '디테일 등급은 V3 에 없다(ADR-0087)')
158
+ assert.equal(byKey.reactive!.ratio, undefined)
159
+ assert.equal(byKey.reactive!.blocked, 'figure.text.maturity-blocked-no-binding')
160
+
161
+ assert.equal(byKey.scale!.ratio, 1, `두 부품이면 ${BOARD_ASSETS} 개 목표를 넘긴다`)
162
+ assert.equal(byKey.consistency!.ratio, 1, '재질 둘은 한도 셋 안이다')
163
+ // Two colours, each half of the footprint, no background: entropy ln 2 over the ceiling ln 4.
164
+ assert.equal(byKey.readable!.ratio, Math.log(2) / Math.log(LIMITS.distinctMaterials + 1))
165
+ assert.equal(byKey.readable!.reading, '2')
166
+ })
167
+
168
+ test('재질이 한도를 넘으면 일관성이 한 칸 내려간다 — V2 의 셋 중 하나', () => {
169
+ let asset = withPalette('t1', 't2', 't3')
170
+ asset = addV3Box(asset, cube('a', 0))
171
+ asset = addV3Box(asset, cube('b', 200, 't1'))
172
+ asset = addV3Box(asset, cube('c', 400, 't2'))
173
+ asset = addV3Box(asset, cube('d', 600, 't3'))
174
+ const report = v3ReportOf(asset)!
175
+ assert.equal(report.distinctMaterials, 4)
176
+ const consistency = report.axes.find(axis => axis.key === 'consistency')!
177
+ assert.equal(consistency.ratio, 2 / 3)
178
+ assert.equal(consistency.readingKey, 'figure.text.maturity-consistency-faults')
179
+ })
180
+
181
+ test('한 색이 발자국을 다 덮으면 식별성은 0 이다 — 위에서 보면 한 덩어리', () => {
182
+ let asset = createV3Asset('Probe')
183
+ asset = addV3Box(asset, { id: 'slab', size: { x: 400, y: 20, z: 400 }, position: { x: 0, y: 10, z: 0 } })
184
+ asset = addV3Box(asset, { id: 'under', size: { x: 100, y: 5, z: 100 }, position: { x: 0, y: 2.5, z: 0 } })
185
+ assert.equal(v3ReportOf(asset)!.axes.find(axis => axis.key === 'readable')!.ratio, 0, '아래 부품은 가려진다')
186
+ })
187
+
188
+ test('추이 — 1 에서 1만까지 1·2·5, 삼각형과 draw call 은 오르고 정점 버퍼와 재질은 평평하다', () => {
189
+ assert.deepEqual(decadeSteps(1, 100), [1, 2, 5, 10, 20, 50, 100])
190
+ let asset = createV3Asset('Probe')
191
+ asset = addV3Box(asset, cube('a', 0))
192
+ asset = addV3Box(asset, cube('b', 200))
193
+ const report = v3ReportOf(asset)!
194
+ const last = report.curve[report.curve.length - 1]!
195
+ assert.equal(report.curve[0]!.instances, 1)
196
+ assert.equal(last.instances, 10000)
197
+ assert.equal(last.triangles, report.cost.triangles * 10000)
198
+ assert.equal(last.drawCalls, report.cost.groups * 10000)
199
+ assert.ok(report.curve.every(p => p.geometries === report.distinctShapes && p.materials === report.distinctMaterials))
200
+
201
+ const layout = curveLayout(report.curve)!
202
+ assert.deepEqual(layout.lines.map(line => [line.key, line.flat, line.dashed]), [
203
+ ['triangles', false, false],
204
+ ['drawCalls', false, false],
205
+ ['geometries', true, false],
206
+ ['materials', true, true]
207
+ ])
208
+ assert.equal(layout.overlap, 1, '형상 하나 · 재질 하나 — 평평한 선이 겹친다고 적는다')
209
+ assert.deepEqual(layout.xTicks.map(t => t.label), ['1', '10', '100', '1k', '10k'])
210
+ })
211
+
212
+ test('화면은 V2 리포트의 차례로 그린다 — 개수 · 축 · 한도 · 그리는 비용 · 추이 · 발행 판정', () => {
213
+ const order = ['this.renderVerdict(report)', 'this.renderAxes(report)', 'this.renderBudgets(report)', 'this.renderMetrics(report)', 'this.renderCurve()', 'this.renderGate()']
214
+ // From the full report, past the early return that shows only guidance and the release verdict.
215
+ const at = order.map(call => verdict.indexOf(call, verdict.indexOf('${this.renderVerdict(report)}')))
216
+ assert.ok(at.every(i => i > 0), `모든 절을 그린다: ${order.filter((_, i) => at[i] < 0)}`)
217
+ assert.deepEqual([...at].sort((a, b) => a - b), at)
218
+ assert.ok(verdict.includes('...GRADE_SCALE.map('), '눈금은 등급을 셈한 경계에서 그린다 — 따로 적으면 어긋난다')
219
+ assert.ok(verdict.includes('SidePanelStyles'), '옆 칸의 한 모양을 쓴다')
220
+ })
221
+
222
+ test('radar 와 추이 그림의 조각은 svg 템플릿이다 — html 로 만들면 소리 없이 안 그려진다', () => {
223
+ for (const block of verdict.match(/svg`[\s\S]*?<\/svg>/g) ?? []) {
224
+ assert.doesNotMatch(block, /html`\s*<\s*(polygon|line|circle|rect|path|text|g)\b/)
225
+ }
226
+ assert.ok((verdict.match(/svg`/g) ?? []).length >= 8, '전제: 조각을 센다')
227
+ })
228
+
229
+ test('화면이 키를 이어 붙여 찾는 말도 다섯 말 모두에 있다', () => {
230
+ const keys = [
231
+ ...V3_BUDGET_KEYS.map(key => `figure.label.budget-${key}`),
232
+ ...['scale', 'consistency', 'reuse', 'detail', 'reactive', 'readable'].flatMap(key => [`figure.label.axis-${key}`, `figure.label.axis-${key}-short`]),
233
+ 'figure.text.bound-by-groups',
234
+ 'figure.text.bound-by-triangles',
235
+ ...[...maturity.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1]!)
236
+ ]
237
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
238
+ const table = JSON.parse(read(`../translations/${lang}.json`))
239
+ for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
240
+ }
241
+ })
242
+
243
+ test('부품 목록 머리의 값 한 줄 — 삼각형 · 묶음 · 한 화면 몇 대 · 등급, D 면 넘친 한도를 적는다', () => {
244
+ assert.equal(budgetOf(undefined), undefined, '잴 것이 없으면 줄도 없다')
245
+ let asset = createV3Asset('Probe')
246
+ asset = addV3Box(asset, cube('a', 0))
247
+ const line = budgetOf(v3ReportOf(asset))!
248
+ assert.deepEqual(
249
+ { triangles: line.triangles, groups: line.groups, grade: line.grade },
250
+ { triangles: 12, groups: 1, grade: 'A' }
251
+ )
252
+ assert.equal(line.instances, capacityFrom(1, 12)!.instances)
253
+
254
+ let glass = withPalette('glass.a', 'glass.b')
255
+ glass = addV3Box(glass, cube('a', 0, 'glass.a', { transparent: true }))
256
+ glass = addV3Box(glass, cube('b', 200, 'glass.b', { transparent: true }))
257
+ const over = budgetOf(v3ReportOf(glass))!
258
+ assert.equal(over.grade, 'D')
259
+ assert.deepEqual(over.tightest, { key: 'transparent', used: 2, limit: 1 })
260
+
261
+ const parts = read('../client/modeller/figure-v3-parts.ts')
262
+ const drawn = parts.slice(parts.indexOf(' private renderBudget()'), parts.indexOf(' /** 한꺼번에 감추고'))
263
+ assert.ok(drawn.includes("const over = b.grade === 'D'") && drawn.includes("'figure.text.parts-budget-over'"), 'D 에서 넘친 한도를 적는다')
264
+ assert.ok(parts.includes("new CustomEvent('show-maturity'"), '누르면 성숙도로')
265
+ const editor = read('../client/modeller/v3-editor.ts')
266
+ assert.ok(editor.includes('.budget=${this.budget}') && editor.includes("@show-maturity=${() => (this.tab = 'verdict')}"))
267
+ assert.ok(verdict.includes('budget: budgetOf(report)'), '성숙도 탭이 같은 보고서에서 알린다')
268
+ })
@@ -29,7 +29,9 @@ test('복제 · 순서 · 삭제는 figure-model 의 부품 편집으로, 세션
29
29
  })
30
30
 
31
31
  test('작업대에서 가린 부품은 캔버스에서 빠지고, 다른 도형을 열면 풀린다', () => {
32
- assert.match(editor, /drawableV3Parts\(view\.geometry \?\? \[\]\)\.filter\(body => !this\.hiddenParts\.has/)
32
+ assert.ok(editor.includes('.parts=${this.drawnParts(view.geometry, asset)}'))
33
+ const drawn = editor.slice(editor.indexOf(' private drawnParts('), editor.indexOf(' private hold('))
34
+ assert.ok(drawn.includes('parts.filter(body => !this.hiddenParts.has('), '끄는 값을 그릴 때도 가린 부품은 빠진다')
33
35
  const reset = editor.slice(editor.indexOf('protected willUpdate'), editor.indexOf('private run('))
34
36
  assert.match(reset, /this\.hiddenParts = new Set\(\)/)
35
37
  })
@@ -0,0 +1,42 @@
1
+ /*
2
+ * **미리보기 — V2 의 그것** (사용자 2026-09-24: 「미리보기도 v2 가 낫다」). 저작 중인 도형을 도면처럼 등록하고,
3
+ * 기준 크기와 한 · 두 · 세 축을 2배로 늘린 일곱을 나란히 FigureInstance 로 세운다.
4
+ */
5
+ import { test } from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { readFileSync } from 'node:fs'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ import { addV3Box, createV3Asset } from '@hatiolab/figure-model'
11
+
12
+ import { authoredSizeOf, PREVIEW_WAYS, previewInstances } from '../client/modeller/v3-preview-model.ts'
13
+
14
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
15
+
16
+ test('여덟 — 기준과 일곱 조합, 한 줄에 넷, 늘린 축만 두 배 · 높이는 늘 적는다', () => {
17
+ assert.deepEqual(PREVIEW_WAYS.map(w => w.name), ['base', 'x', 'y', 'z', 'x·y', 'x·z', 'y·z', 'x·y·z'])
18
+ const { components, board } = previewInstances('T', { x: 1000, y: 500, z: 400 })
19
+ assert.equal(components.length, 8)
20
+ assert.deepEqual(components.map(c => [c.width, c.depth, c.height]), [
21
+ [1000, 500, 400], [2000, 500, 400], [1000, 1000, 400], [1000, 500, 800],
22
+ [2000, 1000, 400], [2000, 500, 800], [1000, 1000, 800], [2000, 1000, 800]
23
+ ])
24
+ assert.ok(components.every(c => c.type === 'T'))
25
+ assert.deepEqual(board, { width: 10000, height: 2000 }, '가장 큰 것이 들어갈 칸 넷씩 두 줄')
26
+ })
27
+
28
+ test('크기를 선언하지 않은 도형은 부품이 차지하는 범위를 기준 크기로', () => {
29
+ const asset = addV3Box(createV3Asset('Fixed'), { id: 'body', size: { x: 300, y: 200, z: 100 }, position: { x: 0, y: 100, z: 0 } })
30
+ assert.deepEqual(authoredSizeOf(asset), { x: 300, y: 200, z: 100 })
31
+ })
32
+
33
+ test('편집 | 미리보기 — 띠에서 오가고, 미리보기는 도면처럼 등록해 세우며 제 타입으로 남의 도면을 건드리지 않는다', () => {
34
+ const editor = read('../client/modeller/v3-editor.ts')
35
+ assert.ok(editor.includes("(['edit', 'preview'] as const).map("))
36
+ assert.ok(editor.includes('<figure-v3-preview'))
37
+ const preview = read('../client/modeller/figure-v3-preview.ts')
38
+ assert.ok(preview.includes('registerFigureV3(type, asset)') && preview.includes('unregisterFigureV3(this.registered)'))
39
+ assert.ok(preview.includes('`figure-v3-preview-${this.context || \'new\'}`'))
40
+ assert.ok(preview.includes("kind: 'set-state-defaults'"), '이 자세를 기본으로')
41
+ assert.ok(preview.includes('dialShape('), '각도는 다이얼로')
42
+ })
@@ -61,8 +61,10 @@ test('태그 줄은 V2 와 같은 규칙으로 목록이 된다', () => {
61
61
  const page = read('../client/pages/figure-v3-modeller-page.ts')
62
62
 
63
63
  test('화면이 속성 탭을 걸고, 명령은 세션의 이력 하나로, 분류 · 태그는 행으로 간다', () => {
64
- assert.ok(/renderTab\('properties', 'tune'/.test(page), '속성 탭')
65
- assert.ok(/<figure-v3-properties\s+\?off=/.test(page), '감추되 떼지 않는다')
64
+ const editor = read('../client/modeller/v3-editor.ts')
65
+ assert.ok(/renderTab\('properties', 'tune'/.test(editor), '속성 탭은 편집기의 오른쪽 칸에')
66
+ assert.ok(/<figure-v3-properties\s+slot="figure"/.test(page), '도형 속성은 부품을 고르지 않았을 때 속성 탭에 선다')
67
+ assert.ok(editor.includes('<slot name="figure"></slot>'))
66
68
  assert.ok(/@v3-command=\$\{this\.onCommand\}/.test(page) && /@v3-catalog=\$\{this\.onCatalog\}/.test(page))
67
69
  const run = page.slice(page.indexOf('private runCommand'), page.indexOf('private onCatalog'))
68
70
  assert.ok(/this\.session\.command\(actionOf\(\)\)/.test(run), '명령은 세션을 지난다 — 되돌리기가 먹는다')
@@ -0,0 +1,85 @@
1
+ /*
2
+ * **편집 캔버스는 V2 처럼 things-scene 편집 모드 위에 선다** (사용자 2026-09-24: 「v2 방식으로 하면 안되냐」).
3
+ *
4
+ * 부품마다 `figure-v3-part` 컴포넌트로 판에 세우면 고르기 · 주황 테두리 · 기즈모와 스냅 · 박스 선택 · 카메라(감속 ·
5
+ * 커서 쪽 확대 · 더블클릭 맞추기 · 시점 1~6) · 스튜디오 환경 · 좌표 바탕 · 조명과 그림자가 씬에서 온다. 여기서는 그
6
+ * 배선과, 몸체 ↔ 컴포넌트의 대응이 왕복하는지를 잰다.
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { readFileSync } from 'node:fs'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ import { changedKeys, componentOf, gizmoResultOf } from '../client/modeller/v3-stage-model.ts'
14
+ import type { V3RenderPart } from '../client/modeller/v3-renderer.ts'
15
+
16
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
17
+ const canvas = read('../client/modeller/v3-canvas.ts')
18
+ const board = { width: 2000, height: 2000 }
19
+
20
+ const turned = (deg: number) => {
21
+ const a = (deg * Math.PI) / 180
22
+ return [
23
+ [Math.cos(a), 0, Math.sin(a)],
24
+ [0, 1, 0],
25
+ [-Math.sin(a), 0, Math.cos(a)]
26
+ ]
27
+ }
28
+ const box = (t: number[], r = turned(0)): V3RenderPart => ({
29
+ id: 'block',
30
+ shape: { provider: 'box', dimensions: { width: 200, height: 100, depth: 60 } },
31
+ pose: { r, t },
32
+ material: { token: 'palette.primary' }
33
+ })
34
+
35
+ test('things-scene 의 판을 편집 모드로 세우고, 보기 설정은 판의 상태 이름 그대로 넣는다', () => {
36
+ assert.ok(canvas.includes("import { create, Model, niceGridStep } from '@hatiolab/things-scene'"))
37
+ assert.match(canvas, /type: 'model-layer'/)
38
+ assert.match(canvas, /mode: 1\n/, '편집 모드 — 시점 단추 · 기즈모 · 도움말 · 단축키')
39
+ assert.ok(canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'))
40
+ assert.ok(canvas.includes('pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2)'), 'V2 의 두 배 표본')
41
+ assert.doesNotMatch(canvas, /new THREE\.|WebGLRenderer|OrbitControls|TransformControls/, '제 장면을 짜지 않는다')
42
+ })
43
+
44
+ test('몸체는 자기 상자로 서고, 씬의 낱말(width·depth·height · left·top·zPos · 회전)로 적힌다', () => {
45
+ const state = componentOf(box([100, 50, -30]), { 'palette.primary': '#123456' }, board)!
46
+ assert.equal(state.type, 'figure-v3-part')
47
+ assert.equal(state.name, 'block')
48
+ assert.deepEqual([state.width, state.depth, state.height], [200, 100, 60])
49
+ assert.equal(state.left, 100 + 1000 - 100)
50
+ assert.equal(state.top, -30 + 1000 - 30)
51
+ assert.equal(state.zPos, 0, '가운데가 y 50, 높이 100 이면 바닥이 0')
52
+ assert.equal(state.color, '#123456')
53
+ })
54
+
55
+ test('기즈모가 옮기면 원점이, 돌리면 각도가, 늘리면 자기 축의 배율이 돌아온다', () => {
56
+ const part = box([100, 50, -30])
57
+ const at = componentOf(part, {}, board)!
58
+ assert.deepEqual(gizmoResultOf(part, at, board), {}, '안 움직였으면 아무것도')
59
+
60
+ const moved = gizmoResultOf(part, { ...at, left: (at.left as number) + 40, zPos: 20 }, board)
61
+ assert.deepEqual(moved.position!.map(Math.round), [140, 70, -30])
62
+ assert.equal(moved.rotation, undefined)
63
+
64
+ const spun = gizmoResultOf(part, { ...at, rotation: -Math.PI / 6 }, board)
65
+ assert.deepEqual(spun.rotation!.map(v => Math.round(v * 10) / 10 + 0), [0, 30, 0])
66
+
67
+ const grown = gizmoResultOf(part, { ...at, width: 400, left: (at.left as number) - 100 }, board)
68
+ assert.deepEqual(grown.scale!.map(v => Math.round(v * 1000) / 1000), [2, 1, 1])
69
+ assert.equal(grown.position, undefined, '가운데서 양쪽으로 늘리면 원점은 그대로')
70
+ })
71
+
72
+ test('돌린 몸체도 왕복한다 — 판에 적은 상태를 그대로 되읽으면 아무것도 안 바뀐다', () => {
73
+ const part = box([10, 50, 20], turned(35))
74
+ const at = componentOf(part, {}, board)!
75
+ assert.deepEqual(gizmoResultOf(part, at, board), {})
76
+ assert.equal(changedKeys(at, { ...at }), null, '같은 값은 다시 넣지 않는다 — 씬이 메아리를 내지 않게')
77
+ assert.deepEqual(changedKeys(at, { ...at, left: 1 }), { left: 1 })
78
+ })
79
+
80
+ test('고르기와 끌기는 이름으로 알리고, 스스로 적은 것의 메아리는 끌기로 읽지 않는다', () => {
81
+ assert.ok(canvas.includes("root.on('selected'"))
82
+ assert.ok(canvas.includes("if (this.writing) return"))
83
+ assert.ok(canvas.includes("new CustomEvent('part-transformed', { detail: { id: part.id, result }"))
84
+ assert.ok(canvas.includes('setTimeout(() => this.sync())'), '받지 못한 변경은 입력이 둔 자리로 돌아간다')
85
+ })
@@ -0,0 +1,25 @@
1
+ /*
2
+ * The editing board only grows while a figure is edited (user 2026-09-24: 「조작값을 변경하면, 편집기가 자꾸 리프레시」).
3
+ * A board that outgrows itself is stood up again and the camera is lost; a raised arm made the height larger and the
4
+ * height was measured afresh each time.
5
+ */
6
+ import { test } from 'node:test'
7
+ import assert from 'node:assert/strict'
8
+
9
+ import { addV3Box, compileV3Asset, createV3Asset } from '@hatiolab/figure-model'
10
+
11
+ import { drawableV3Parts } from '../client/modeller/v3-renderer.ts'
12
+ import { boardFor } from '../client/modeller/v3-stage-model.ts'
13
+
14
+ const partsOf = (height: number) =>
15
+ drawableV3Parts(
16
+ compileV3Asset(addV3Box(createV3Asset('Probe'), { id: 'post', size: { x: 100, y: height, z: 100 }, position: { x: 0, y: height / 2, z: 0 } }))
17
+ .evaluate().geometry
18
+ )
19
+
20
+ test('the board keeps the height it had: a part that shrinks back does not stand a new board up', () => {
21
+ const tall = boardFor(partsOf(2000))
22
+ const after = boardFor(partsOf(500), tall)
23
+ assert.equal(after.depth, tall.depth)
24
+ assert.ok(boardFor(partsOf(3000), tall).depth > tall.depth, 'it still grows')
25
+ })
@@ -20,24 +20,14 @@ const fixed = (text: string) =>
20
20
  .replace(/var\(--[\w-]+,\s*#[0-9a-fA-F]{3,8}\)/g, '')
21
21
  .match(/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b(?![\w-])/g) ?? []
22
22
 
23
- test('캔버스와 편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', () => {
24
- assert.deepEqual(fixed(canvas), [], 'v3-canvas.ts')
23
+ test('편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', () => {
25
24
  assert.deepEqual(fixed(editor), [], 'v3-editor.ts')
26
25
  })
27
26
 
28
- test('캔버스 바탕은 CSS 와 3D 가 같은 토큰에서 온다', () => {
29
- assert.match(canvas, /background: var\(--md-sys-color-surface-container\)/)
30
- assert.match(canvas, /this\.scene\.background = new THREE\.Color\(colour\)/)
31
- assert.match(canvas, /getComputedStyle\(this\)\.backgroundColor/, '3D 바탕이 호스트의 바탕을 읽는다')
32
- })
33
-
34
- test('테마를 바꾸면 3D 바탕을 다시 읽는다 — 한 번만 읽으면 가운데만 밝은 판이 남는다', () => {
35
- assert.match(canvas, /observe\(document\.body, \{ attributes: true, attributeFilter: \['class'\] \}\)/)
36
- const onChange = canvas.slice(canvas.indexOf('new MutationObserver'), canvas.indexOf('this.themeObserver.observe'))
37
- assert.match(onChange, /this\.paintBackground\(\)/)
38
- assert.match(onChange, /this\.draw\(\)/, '다시 읽고 그리지 않으면 다음 움직임까지 옛 색이다')
39
- const leave = canvas.slice(canvas.indexOf('disconnectedCallback()'))
40
- assert.match(leave, /this\.themeObserver\?\.disconnect\(\)/, '떠난 캔버스가 body 를 계속 지켜본다')
27
+ test('캔버스 바탕은 V2 의 작업대 회색 — 판의 하늘색이라 환경 탭에서 바꾼다', () => {
28
+ assert.ok(canvas.includes('background-color: ${unsafeCSS(WORKBENCH)}'), '씬이 서기 전 · 2D 쪽도 같은 색')
29
+ assert.ok(canvas.includes("sky: 'color'") && canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'), '3D 바탕은 보기 설정의 skyColor')
30
+ assert.match(read('../client/modeller/figure-view.ts'), /skyColor: WORKBENCH/)
41
31
  })
42
32
 
43
33
  test('눌린 단추와 오류 칸은 셸이 두 테마 대비를 맞춘 칸을 쓴다', () => {
@@ -50,18 +40,19 @@ test('눌린 단추와 오류 칸은 셸이 두 테마 대비를 맞춘 칸을
50
40
  assert.match(error, /--md-sys-color-on-error-container\)/)
51
41
  })
52
42
 
53
- test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일과 오른쪽 칸이 폭을 먼저 가져간다', () => {
43
+ test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일이 폭을 먼저 가져간다', () => {
44
+ /* The widths - 200 | 1fr | 340, 160 | 300 under 860px, one column with the canvas first under 560px - are
45
+ ox-three-pane's, measured in @operato/layout's own test. The editor must not write the grid again. */
54
46
  assert.match(editor, /container-type: inline-size/)
55
- assert.doesNotMatch(editor, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 오른쪽 칸 위로 넘친다')
56
- assert.match(editor, /@container \(max-width: 659px\)/, '세 칸의 최소 폭 합(660) 아래에서 두 칸')
57
- const one = editor.slice(editor.indexOf('@container (max-width: 419px)'))
58
- assert.match(one, /grid-template-columns: minmax\(0, 1fr\)/, '420 아래에서 한 줄')
59
- assert.match(one, /figure-v3-canvas \{\s*order: -1;/, '한 줄에서는 캔버스가 먼저')
47
+ assert.doesNotMatch(editor, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 넘친다')
48
+ assert.match(editor, /<ox-three-pane>/, '세 칸은 공용 틀')
49
+ assert.doesNotMatch(editor, /grid-template-columns/, '편집기가 격자를 다시 쓰지 않는다')
60
50
  })
61
51
 
62
- test('넘침이 없으니 오른쪽 칸은 편집기를 가리지 않는다 — 가림은 우회였다', () => {
63
- const page = read('../client/pages/figure-v3-modeller-page.ts')
64
- const aside = page.slice(page.indexOf(' aside {'), page.indexOf('}', page.indexOf(' aside {')))
52
+ test('양쪽 칸은 띠에서 접는다 — 접으면 캔버스가 그 폭을 갖고, 오른쪽 칸은 무엇도 가리지 않는다', () => {
53
+ assert.ok(editor.includes('.pane=${this.panes}'), '띠가 그 틀을 접는다')
54
+ assert.match(editor, /querySelector<OxThreePane>\('ox-three-pane'\)/, '띠에 건네는 틀은 편집기의 세 칸')
55
+ const aside = editor.slice(editor.indexOf(' aside {'), editor.indexOf('}', editor.indexOf(' aside {')))
65
56
  assert.doesNotMatch(aside, /z-index/)
66
- assert.match(aside, /border-left: 1px solid var\(--md-sys-color-outline-variant\)/, '칸 경계선은 남는다')
57
+ assert.doesNotMatch(aside, /border-left/, '칸 경계선은 틀이 긋는다 — 두 줄이 되지 않게')
67
58
  })