@things-factory/figure-ui 10.1.16 โ†’ 10.1.18

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 (78) hide show
  1. package/client/modeller/figure-animations.ts +331 -124
  2. package/client/modeller/figure-canvas.ts +32 -48
  3. package/client/modeller/figure-capabilities.ts +10 -10
  4. package/client/modeller/figure-inspector.ts +39 -28
  5. package/client/modeller/figure-parts.ts +1 -1
  6. package/client/modeller/figure-preview.ts +151 -41
  7. package/client/modeller/figure-report.ts +24 -6
  8. package/client/modeller/figure-settings.ts +45 -2
  9. package/client/modeller/figure-side.ts +6 -6
  10. package/client/modeller/figure-source.ts +62 -32
  11. package/client/modeller/figure-thumbnail.ts +5 -5
  12. package/client/modeller/figure-view.ts +53 -0
  13. package/client/modeller/part-edits.ts +36 -36
  14. package/client/modeller/proposal.ts +27 -4
  15. package/client/pages/figure-modeller-page.ts +65 -58
  16. package/dist-client/modeller/figure-animations.d.ts +37 -8
  17. package/dist-client/modeller/figure-animations.js +293 -122
  18. package/dist-client/modeller/figure-animations.js.map +1 -1
  19. package/dist-client/modeller/figure-canvas.d.ts +7 -7
  20. package/dist-client/modeller/figure-canvas.js +27 -40
  21. package/dist-client/modeller/figure-canvas.js.map +1 -1
  22. package/dist-client/modeller/figure-capabilities.d.ts +2 -2
  23. package/dist-client/modeller/figure-capabilities.js +9 -9
  24. package/dist-client/modeller/figure-capabilities.js.map +1 -1
  25. package/dist-client/modeller/figure-inspector.d.ts +16 -5
  26. package/dist-client/modeller/figure-inspector.js +38 -27
  27. package/dist-client/modeller/figure-inspector.js.map +1 -1
  28. package/dist-client/modeller/figure-parts.d.ts +1 -1
  29. package/dist-client/modeller/figure-parts.js +1 -1
  30. package/dist-client/modeller/figure-parts.js.map +1 -1
  31. package/dist-client/modeller/figure-preview.d.ts +56 -6
  32. package/dist-client/modeller/figure-preview.js +132 -39
  33. package/dist-client/modeller/figure-preview.js.map +1 -1
  34. package/dist-client/modeller/figure-report.d.ts +9 -0
  35. package/dist-client/modeller/figure-report.js +23 -5
  36. package/dist-client/modeller/figure-report.js.map +1 -1
  37. package/dist-client/modeller/figure-settings.js +42 -2
  38. package/dist-client/modeller/figure-settings.js.map +1 -1
  39. package/dist-client/modeller/figure-side.d.ts +2 -2
  40. package/dist-client/modeller/figure-side.js +5 -5
  41. package/dist-client/modeller/figure-side.js.map +1 -1
  42. package/dist-client/modeller/figure-source.d.ts +25 -15
  43. package/dist-client/modeller/figure-source.js +42 -21
  44. package/dist-client/modeller/figure-source.js.map +1 -1
  45. package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
  46. package/dist-client/modeller/figure-thumbnail.js +3 -3
  47. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  48. package/dist-client/modeller/figure-view.d.ts +45 -0
  49. package/dist-client/modeller/figure-view.js +27 -0
  50. package/dist-client/modeller/figure-view.js.map +1 -1
  51. package/dist-client/modeller/part-edits.d.ts +16 -16
  52. package/dist-client/modeller/part-edits.js +35 -35
  53. package/dist-client/modeller/part-edits.js.map +1 -1
  54. package/dist-client/modeller/proposal.d.ts +1 -1
  55. package/dist-client/modeller/proposal.js +16 -3
  56. package/dist-client/modeller/proposal.js.map +1 -1
  57. package/dist-client/pages/figure-modeller-page.d.ts +9 -10
  58. package/dist-client/pages/figure-modeller-page.js +61 -54
  59. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  60. package/dist-client/route.d.ts +1 -1
  61. package/dist-client/tsconfig.tsbuildinfo +1 -1
  62. package/dist-server/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +5 -5
  64. package/test/ai-proposal-contract.test.ts +433 -12
  65. package/test/expect.ts +55 -0
  66. package/test/figure-source.test.ts +572 -0
  67. package/test/i18n-prefix-guard.test.ts +9 -12
  68. package/{client/modeller โ†’ test}/part-edits.test.ts +50 -45
  69. package/translations/en.json +46 -35
  70. package/translations/ja.json +16 -21
  71. package/translations/ko.json +56 -45
  72. package/translations/ms.json +16 -21
  73. package/translations/zh.json +16 -21
  74. package/client/modeller/figure-ask.ts +0 -265
  75. package/client/modeller/figure-source.test.ts +0 -352
  76. package/dist-client/modeller/figure-ask.d.ts +0 -40
  77. package/dist-client/modeller/figure-ask.js +0 -282
  78. package/dist-client/modeller/figure-ask.js.map +0 -1
@@ -0,0 +1,572 @@
1
+ import assert from 'node:assert/strict'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { describe, it, test } from 'node:test'
4
+
5
+ import { expect } from './expect.ts'
6
+
7
+ import { existsSync, readFileSync } from 'fs'
8
+ import { dirname, join } from 'path'
9
+
10
+ import { toFigureSource, fromFigureSource, partToFigure, partFromFigure } from '../client/modeller/figure-source.ts'
11
+ import type { FigureDraft, PartModel } from '../client/modeller/figure-source.ts'
12
+ import type { FigurePlacement, FigureSource } from '@hatiolab/figure-model'
13
+
14
+ /**
15
+ * ์”ฌ ๋ชจ๋ธ โ†” `FigureSource`.
16
+ *
17
+ * ์ €์ž‘ ์ค‘์˜ ์ •๋ณธ์€ ์”ฌ ๋ชจ๋ธ์ด๊ณ , ํ˜•์‹์€ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ํŒ์ •ํ•  ๋•Œ ์ ‘์–ด์„œ ๋งŒ๋“ ๋‹ค. ๋‘ ์–ดํœ˜๊ฐ€
18
+ * ๋‹ค๋ฅด๋ฏ€๋กœ **์˜ฎ๊ธฐ๋Š” ์ผ์ด ํ•œ ๊ณณ์—๋งŒ** ์žˆ์–ด์•ผ ํ•˜๊ณ , ๊ทธ ํ•œ ๊ณณ์ด ๋งž๋Š”์ง€๋ฅผ ์—ฌ๊ธฐ์„œ ๋ชป ๋ฐ•๋Š”๋‹ค.
19
+ *
20
+ * ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒƒ์€ ์™•๋ณต์ด๋‹ค. ์ ‘์—ˆ๋‹ค ํŽด๋ฉด ๊ฐ™์•„์•ผ ํ•˜๊ณ  ํŽด์„œ ์ ‘์–ด๋„ ๊ฐ™์•„์•ผ ํ•œ๋‹ค โ€”
21
+ * ์–ด๋А ํ•œ์ชฝ์—์„œ๋งŒ ๋งž์œผ๋ฉด AI ๊ฐ€ ์ œ์•ˆํ•ด ์˜จ ์ •๋ณธ์„ ํŽธ์ง‘ํ•˜๋Š” ์ˆœ๊ฐ„ ๊ฐ’์ด ์ƒŒ๋‹ค.
22
+ */
23
+
24
+ /** ํŒ 3000 x 2000 x 800 โ€” ์„ธ ์ถ•์ด ๋‹ค ๋‹ฌ๋ผ์•ผ ์ถ•์„ ๋ฐ”๊ฟ” ์จ๋„ ์‹œํ—˜์ด ์žก๋Š”๋‹ค. */
25
+ const DRAFT: FigureDraft = {
26
+ width: 3000,
27
+ height: 2000,
28
+ depth: 800,
29
+ figureType: 'AGV',
30
+ detailLevel: 'L'
31
+ }
32
+
33
+ const PART: PartModel = {
34
+ type: 'figure-part',
35
+ name: 'deck',
36
+ primitive: 'cube',
37
+ left: 1100,
38
+ top: 700,
39
+ width: 800,
40
+ height: 600,
41
+ zPos: 300,
42
+ depth: 200,
43
+ token: 'palette.primary'
44
+ }
45
+
46
+ describe('์ ‘๊ธฐ โ€” ์”ฌ ๋ชจ๋ธ์„ ํ˜•์‹์œผ๋กœ', () => {
47
+ it('ํŒ์ด ๊ธฐ์ค€ ์ƒ์ž๊ฐ€ ๋œ๋‹ค โ€” ๋†’์ด๋Š” ํŒ์˜ depth ๋‹ค', () => {
48
+ const { base } = toFigureSource(DRAFT, [])
49
+ expect(base).toEqual({ x: 3000, y: 800, z: 2000 })
50
+ })
51
+
52
+ it('๋ถ€ํ’ˆ ์ž๋ฆฌ๊ฐ€ ํŒ ์ค‘์‹ฌ ๊ธฐ์ค€์ด ๋œ๋‹ค', () => {
53
+ const [part] = toFigureSource(DRAFT, [PART]).parts
54
+
55
+ // ํ‰๋ฉด ์ค‘์‹ฌ (1100+400, 700+300) = (1500, 1000), ํŒ ์ค‘์‹ฌ (1500, 1000) โ†’ ์›์ 
56
+ expect(part.transform.position.x).toBe(0)
57
+ expect(part.transform.position.z).toBe(0)
58
+ })
59
+
60
+ it('zPos ๋Š” ๋ฐ‘๋ฉด์ด๋‹ค โ€” ์ค‘์‹ฌ์œผ๋กœ ์˜ฌ๋ ค ์ ๋Š”๋‹ค', () => {
61
+ const [part] = toFigureSource(DRAFT, [PART]).parts
62
+
63
+ // ๋ฐ‘๋ฉด 300 + ๋‘๊ป˜ 200/2 = ์ค‘์‹ฌ 400. ํŒ ๋†’์ด 800 ์˜ ์ ˆ๋ฐ˜์„ ๋นผ๋ฉด 0
64
+ expect(part.transform.position.y).toBe(0)
65
+ })
66
+
67
+ it('๊ฐ€์šด๋ฐ๋ฅผ ๋ฒ—์–ด๋‚œ ๋ถ€ํ’ˆ์ด ๊ทธ๋งŒํผ ์–ด๊ธ‹๋‚˜ ์ ํžŒ๋‹ค', () => {
68
+ const moved = { ...PART, left: 1200, top: 900, zPos: 500 }
69
+ const [part] = toFigureSource(DRAFT, [moved]).parts
70
+
71
+ expect(part.transform.position).toEqual({ x: 100, y: 200, z: 200 })
72
+ })
73
+
74
+ it('ํฌ๊ธฐ๊ฐ€ ์ถ• ์ด๋ฆ„์œผ๋กœ ๋ฐ”๋€๋‹ค โ€” ๋‘๊ป˜๊ฐ€ y ๋‹ค', () => {
75
+ const [part] = toFigureSource(DRAFT, [PART]).parts
76
+ expect(part.transform.size).toEqual({ x: 800, y: 200, z: 600 })
77
+ })
78
+
79
+ /*
80
+ ์”ฌ์€ ๋ผ๋””์•ˆ์„ ์“ฐ๊ณ  ์ด๋ฆ„์ด ๋’ค์„ž์—ฌ ์žˆ๋‹ค โ€” rotationY ๊ฐ€ three ์˜ z ๋กœ ๊ฐ„๋‹ค.
81
+ ํ˜•์‹์€ ๋„๋ฅผ ์“ฐ๊ณ  ์ถ• ์ด๋ฆ„์ด ๊ทธ๋Œ€๋กœ๋‹ค.
82
+ */
83
+ it('ํšŒ์ „์ด ๋„๋กœ ๋ฐ”๋€Œ๊ณ  ์ถ•์ด ์ œ์ž๋ฆฌ๋ฅผ ์ฐพ๋Š”๋‹ค', () => {
84
+ const turned = { ...PART, rotationX: Math.PI / 6, rotation: Math.PI / 2, rotationY: Math.PI }
85
+ const [part] = toFigureSource(DRAFT, [turned]).parts
86
+
87
+ expect(part.transform.rotation).toEqual({ x: 30, y: -90, z: 180 })
88
+ })
89
+
90
+ it('ํšŒ์ „์ด ์—†์œผ๋ฉด ํ•„๋“œ๋ฅผ ๋‘์ง€ ์•Š๋Š”๋‹ค', () => {
91
+ const [part] = toFigureSource(DRAFT, [PART]).parts
92
+ expect(part.transform.rotation).toBeUndefined()
93
+ })
94
+
95
+ it('์žฌ์งˆ์ด ํ•œ ๋ฉ์–ด๋ฆฌ๋กœ ๋ชจ์ธ๋‹ค', () => {
96
+ const [part] = toFigureSource(DRAFT, [{ ...PART, preset: 'metal', flatShading: false }]).parts
97
+ expect(part.material).toEqual({ token: 'palette.primary', preset: 'metal', flatShading: false })
98
+ })
99
+
100
+ it('๋‹จ๋ฉด ๊ฐ’์ด shape ์œผ๋กœ ๋ชจ์ธ๋‹ค', () => {
101
+ const [part] = toFigureSource(DRAFT, [{ ...PART, primitive: 'rect', round: 40 }]).parts
102
+ expect(part.shape).toEqual({ round: 40 })
103
+ })
104
+
105
+ it('๋‹จ๋ฉด ๊ฐ’์ด ์—†์œผ๋ฉด shape ์„ ๋‘์ง€ ์•Š๋Š”๋‹ค', () => {
106
+ expect(toFigureSource(DRAFT, [PART]).parts[0].shape).toBeUndefined()
107
+ })
108
+ })
109
+
110
+ describe('ํŽด๊ธฐ โ€” ํ˜•์‹์„ ์”ฌ ๋ชจ๋ธ๋กœ', () => {
111
+ const SOURCE: FigureSource = {
112
+ type: 'AGV',
113
+ base: { x: 3000, y: 800, z: 2000 },
114
+ detailLevel: 'L',
115
+ parts: [
116
+ {
117
+ name: 'deck',
118
+ primitive: 'cube',
119
+ transform: { position: { x: 100, y: 200, z: 200 }, size: { x: 800, y: 200, z: 600 } },
120
+ material: { token: 'palette.primary' }
121
+ }
122
+ ]
123
+ }
124
+
125
+ it('๊ธฐ์ค€ ์ƒ์ž๊ฐ€ ํŒ์ด ๋œ๋‹ค', () => {
126
+ const { draft } = fromFigureSource(SOURCE)
127
+ expect(draft).toMatchObject({ width: 3000, height: 2000, depth: 800 })
128
+ })
129
+
130
+ it('์ค‘์‹ฌ ๊ธฐ์ค€ ์ž๋ฆฌ๊ฐ€ ์”ฌ์˜ ๋ชจ์„œ๋ฆฌยท๋ฐ‘๋ฉด์œผ๋กœ ๋ฐ”๋€๋‹ค', () => {
131
+ const [part] = fromFigureSource(SOURCE).parts
132
+ expect(part).toMatchObject({ left: 1200, top: 900, zPos: 500 })
133
+ })
134
+
135
+ it('๋ถ€ํ’ˆ์ด ์”ฌ ํƒ€์ž…์„ ๊ฐ–๋Š”๋‹ค โ€” ํŒ์— ๊ทธ๋Œ€๋กœ ๋†“์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค', () => {
136
+ expect(fromFigureSource(SOURCE).parts[0].type).toBe('figure-part')
137
+ })
138
+ })
139
+
140
+ /*
141
+ ์™•๋ณต.
142
+
143
+ ์ด๊ฒƒ์ด ์ด ๋ชจ๋“ˆ์˜ ์กด์žฌ ์ด์œ ๋‹ค. ํ•œ์ชฝ์—์„œ๋งŒ ๋งž์œผ๋ฉด AI ๊ฐ€ ์ œ์•ˆํ•ด ์˜จ ์ •๋ณธ์„ ํŽด์„œ
144
+ ํŽธ์ง‘ํ•˜๊ณ  ๋‹ค์‹œ ์ ‘๋Š” ์ˆœ๊ฐ„ ๊ฐ’์ด ์ƒŒ๋‹ค โ€” ๊ทธ๊ฒƒ๋„ ์กฐ์šฉํžˆ.
145
+ */
146
+ /**
147
+ * ํ˜•์‹์ด ๊ฐ€์ง„ ๊ฒƒ์„ ๋˜๋„๋ก ๋‹ค ๋‹ด์€ ์ •๋ณธ. ์•ˆ ์“ฐ์ด๋Š” ํ•„๋“œ๋„ ๋„ฃ๋Š”๋‹ค.
148
+ *
149
+ * describe ๋ฐ–์— ๋‘”๋‹ค โ€” ์•„๋ž˜ ใ€Œํ”ฝ์Šค์ฒ˜๊ฐ€ ํ˜•์‹์„ ๋‹ค ๋‹ด๋Š”๋‹คใ€๊ฐ€ ์ด๊ฒƒ์„ ํ˜•์‹์˜ ํ•„๋“œ ๋ชฉ๋ก๊ณผ
150
+ * ๋งž๋Œ€์–ด ๋ณธ๋‹ค.
151
+ */
152
+ /** ํ˜•์‹์ด ๊ฐ€์ง„ ๊ฒƒ์„ ๋˜๋„๋ก ๋‹ค ๋‹ด์€ ์ •๋ณธ. ์•ˆ ์“ฐ์ด๋Š” ํ•„๋“œ๋„ ๋„ฃ๋Š”๋‹ค. */
153
+ const FULL: FigureSource = {
154
+ version: 1,
155
+ type: 'AGV',
156
+ base: { x: 3000, y: 800, z: 2000 },
157
+ placement: 'floor',
158
+ detailLevel: 'L',
159
+ styleKit: 'factory',
160
+ parts: [
161
+ {
162
+ name: 'deck',
163
+ primitive: 'cube',
164
+ transform: { position: { x: 100, y: 200, z: 200 }, size: { x: 800, y: 200, z: 600 } },
165
+ material: { token: 'palette.primary', preset: 'metal', flatShading: true }
166
+ },
167
+ {
168
+ name: 'wheel',
169
+ primitive: 'cylinder',
170
+ segments: 24,
171
+ transform: {
172
+ position: { x: -700, y: -260, z: 420 },
173
+ size: { x: 180, y: 70, z: 180 },
174
+ rotation: { z: 90 }
175
+ },
176
+ material: { token: 'palette.dark', preset: 'rubber' }
177
+ },
178
+ {
179
+ name: 'panel',
180
+ primitive: 'rect',
181
+ // ์†์„ ํŒ ๋‹จ๋ฉด. ํ•œ ๊ฒน ์•ˆ์ด๋ผ ์ฒ˜์Œ ํŒ ์‹œํ—˜์ด ๋ชป ๋ดค๋˜ ์ž๋ฆฌ๋‹ค.
182
+ shape: { round: 40, hollow: { wall: 8, floor: 4 } },
183
+ transform: {
184
+ position: { x: 0, y: 260, z: -500 },
185
+ size: { x: 400, y: 30, z: 200 },
186
+ rotation: { x: 15, y: -30, z: 45 }
187
+ },
188
+ material: { token: 'palette.accent', transparent: true },
189
+ // ์•„์ง ์•„๋ฌด ์ผ๋„ ์•ˆ ํ•˜๋Š” ํ•„๋“œ๋“ค. ์žƒ์–ด๋ฒ„๋ฆฌ๋ฉด ์•ˆ ๋œ๋‹ค.
190
+ materialSlot: 'cap',
191
+ keepRound: false,
192
+ sizing: 'fixed',
193
+ anchor: { x: 'span', y: 'max', z: 'center' },
194
+ label: { name: 'rpm', source: 'speed', when: 'zoomed' }
195
+ },
196
+ {
197
+ name: 'foot',
198
+ primitive: 'polygon',
199
+ shape: {
200
+ path: [
201
+ { x: -60, y: -40 },
202
+ { x: 60, y: -40 },
203
+ { x: 0, y: 40 }
204
+ ]
205
+ },
206
+ transform: { position: { x: 900, y: -340, z: -600 }, size: { x: 120, y: 80, z: 80 } },
207
+ material: { token: 'palette.neutral' },
208
+ sizing: 'repeat',
209
+ repeat: { axis: 'x', pitch: 200 }
210
+ },
211
+ {
212
+ /*
213
+ ๋ฌผ๊ฑด์„ ๋†“๋Š” ์ž๋ฆฌ์ด๋ฉด์„œ ๋“œ๋‚˜๋“œ๋Š” ๋ฌธ์ด๋‹ค โ€” AGV ์˜ ์ƒํŒ์ด ๊ทธ๋ ‡๋‹ค. **๊ทธ๋ ค์ง€์ง€ ์•Š์œผ๋ฏ€๋กœ**
214
+ ์žฌ์งˆ์ด ์—†๋‹ค.
215
+
216
+ ๊ทธ๋ฆฌ์ง€ ์•Š๋Š” ๊ฒƒ์ด๋ผ ํ™”๋ฉด์œผ๋กœ๋Š” ์™•๋ณต์—์„œ ๋ˆ„๋ฝ๋๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค. ์ž๋ฆฌ๋ฅผ ์žƒ์–ด๋ฒ„๋ฆฐ ๋„ํ˜•์€
217
+ ๋ฉ€์ฉกํ•ด ๋ณด์ด๋ฉด์„œ ์•„๋ฌด๊ฒƒ๋„ ๋ฐ›์ง€ ๋ชปํ•œ๋‹ค.
218
+ */
219
+ name: 'seat',
220
+ primitive: 'cube',
221
+ transform: { position: { x: 0, y: 420, z: 0 }, size: { x: 700, y: 500, z: 500 } },
222
+ capability: { roles: ['slot', 'port-in', 'port-out'], accepts: ['PALLET'], capacity: 2 }
223
+ }
224
+ ],
225
+ /* ์ด ๋„ํ˜•์ด ์ง€๋Š” ๋Šฅ๋ ฅ. ๋ถ€ํ’ˆ์ด ์•„๋‹ˆ๋ผ ๋„ํ˜• ์ „์ฒด์˜ ๊ฒƒ์ด๋‹ค. */
226
+ capabilities: ['FlowNode'],
227
+ /*
228
+ ์›€์ง์ž„. ํŽธ์ง‘๋ฉด์ด ์•„์ง ์•ˆ ๋งŒ์ง€๋Š” ์ž๋ฆฌ์ด๋ฉฐ, **๋ฐ”๋กœ ๊ทธ๋ž˜์„œ ์—ฌ๊ธฐ ์žˆ๋‹ค.**
229
+
230
+ ์ด ํ”ฝ์Šค์ฒ˜๊ฐ€ ใ€Œํ˜•์‹์ด ๊ฐ€์ง„ ๊ฒƒ์„ ๋˜๋„๋ก ๋‹ค ๋‹ด์€ ์ •๋ณธใ€์ธ๋ฐ clip ์ด ๋น ์ ธ ์žˆ์—ˆ๊ณ , ๊ทธ
231
+ ์‚ฌ์ด `toFigureSource` ๊ฐ€ clip ์„ ์•ˆ ์‹ค์—ˆ๋‹ค. ์‹œํ—˜์€ ์ดˆ๋ก์ด์—ˆ๋‹ค โ€” ํ”ฝ์Šค์ฒ˜์— ์—†๋Š” ๊ฒƒ์€
232
+ ์‹œํ—˜์ด ๋ชป ๋ณธ๋‹ค. ํ‘œ๋ณธ ๋กค๋Ÿฌ์— clip ์„ ์ ๊ณ  ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—์„œ ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋Œ๊ณ  ๋‚˜์„œ์•ผ ์•Œ์•˜๋‹ค.
233
+ */
234
+ animations: [
235
+ {
236
+ name: 'spin',
237
+ channels: [
238
+ {
239
+ target: 'wheel',
240
+ path: 'rotation',
241
+ pivot: { x: 0, y: 0, z: 0 },
242
+ keys: [
243
+ { at: 0, value: { x: 0, y: 0, z: 0 } },
244
+ { at: 1, value: { x: 0, y: 360, z: 0 } }
245
+ ]
246
+ }
247
+ ]
248
+ }
249
+ ],
250
+ /*
251
+ ๊ฐ’์ด ๋งŒ๋“œ๋Š” ์ž์„ธ. ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ **๋‹ค๋ฅธ ์นธ์ด๋ผ** ๋”ฐ๋กœ ๋‹ด๋Š”๋‹ค โ€” ๊ฐ™์€ ์ด์œ ๋กœ ์—ฌ๊ธฐ ์žˆ๋‹ค.
252
+ ํ•œ ์นธ์ด ์ƒˆ๋ฉด ์—ด์—ˆ๋‹ค ์ €์žฅํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์กฐ์šฉํžˆ ์ง€์›Œ์ง„๋‹ค.
253
+ */
254
+ parameters: [
255
+ {
256
+ name: 'lift',
257
+ label: 'ํฌํฌ ๋†’์ด',
258
+ range: { unit: 'mm', min: 0, max: 1800 },
259
+ default: 0,
260
+ clip: {
261
+ channels: [
262
+ {
263
+ target: 'wheel',
264
+ path: 'translation',
265
+ keys: [
266
+ { at: 0, value: { x: 0, y: 0, z: 0 } },
267
+ { at: 2.5, value: { x: 0, y: 1800, z: 0 } }
268
+ ]
269
+ }
270
+ ]
271
+ }
272
+ }
273
+ ]
274
+ }
275
+
276
+ describe('์™•๋ณตํ•ด๋„ ๊ฐ™๋‹ค', () => {
277
+
278
+ it('ํŽด์„œ ์ ‘์œผ๋ฉด ๊ทธ๋Œ€๋กœ๋‹ค', () => {
279
+ const { draft, parts } = fromFigureSource(FULL)
280
+ expect(toFigureSource(draft, parts)).toEqual(FULL)
281
+ })
282
+
283
+ it('์ ‘์–ด์„œ ํŽด๋ฉด ๊ทธ๋Œ€๋กœ๋‹ค', () => {
284
+ const { draft, parts } = fromFigureSource(FULL)
285
+ const again = fromFigureSource(toFigureSource(draft, parts))
286
+
287
+ expect(again.draft).toEqual(draft)
288
+ expect(again.parts).toEqual(parts)
289
+ })
290
+
291
+ it('๋‘ ๋ฒˆ ์™•๋ณตํ•ด๋„ ๊ฐ’์ด ๋ฐ€๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€” ๋ผ๋””์•ˆยท๋„๋ฅผ ์˜ค๊ฐ€๋„', () => {
292
+ let source = FULL
293
+ for (let i = 0; i < 5; i++) {
294
+ const { draft, parts } = fromFigureSource(source)
295
+ source = toFigureSource(draft, parts)
296
+ }
297
+ expect(source).toEqual(FULL)
298
+ })
299
+
300
+ it('์›€์ง์ž„๋„ ์‚ด์•„๋‚จ๋Š”๋‹ค โ€” ์—ด์—ˆ๋‹ค ์ €์žฅํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์ง€์›Œ์ง€๋˜ ์ž๋ฆฌ๋‹ค', () => {
301
+ const { draft, parts } = fromFigureSource(FULL)
302
+
303
+ expect(toFigureSource(draft, parts).animations).toEqual(FULL.animations)
304
+ })
305
+
306
+ it('๊ฐ’์ด ๋งŒ๋“œ๋Š” ์ž์„ธ๋„ ์‚ด์•„๋‚จ๋Š”๋‹ค โ€” ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋‹ค๋ฅธ ์นธ์ด๋ผ ๋”ฐ๋กœ ์ƒ ์ˆ˜ ์žˆ๋‹ค', () => {
307
+ const { draft, parts } = fromFigureSource(FULL)
308
+
309
+ expect(toFigureSource(draft, parts).parameters).toEqual(FULL.parameters)
310
+ })
311
+
312
+ it('์•ˆ ์“ฐ์ด๋Š” ํ•„๋“œ๋„ ์‚ด์•„๋‚จ๋Š”๋‹ค โ€” ์กฐ์šฉํžˆ ์‚ฌ๋ผ์ง€๋ฉด ์ €์ž‘์ž๊ฐ€ ์•Œ ๊ธธ์ด ์—†๋‹ค', () => {
313
+ const { draft, parts } = fromFigureSource(FULL)
314
+ const panel = toFigureSource(draft, parts).parts.find(part => part.name === 'panel')!
315
+
316
+ expect(panel.materialSlot).toBe('cap')
317
+ expect(panel.sizing).toBe('fixed')
318
+ expect(panel.label).toEqual({ name: 'rpm', source: 'speed', when: 'zoomed' })
319
+ })
320
+ })
321
+
322
+ /*
323
+ ํ”ฝ์Šค์ฒ˜๊ฐ€ ํ˜•์‹์„ ๋”ฐ๋ผ๊ฐ€๋Š”์ง€.
324
+
325
+ **์ด ํŒŒ์ผ์—์„œ ํ•„๋“œ๊ฐ€ ๋„ค ๋ฒˆ ์ƒœ๋‹ค** โ€” `ports` ยท `animations` ยท `anchor` ยท `shape.hollow`.
326
+ ๋„ค ๋ฒˆ ๋‹ค ์™•๋ณต ์‹œํ—˜์€ ์ดˆ๋ก์ด์—ˆ๋‹ค. ์œ„์˜ `FULL` ์ด ใ€Œํ˜•์‹์ด ๊ฐ€์ง„ ๊ฒƒ์„ ๋˜๋„๋ก ๋‹ค ๋‹ด์€
327
+ ์ •๋ณธใ€์ด๋ผ๊ณ  ์ ํ˜€ ์žˆ์—ˆ์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ๊ทธ๋•Œ๊ทธ๋•Œ ์†์œผ๋กœ ์ฑ„์šด ๊ฒƒ์ด๋ผ, ํ˜•์‹์— ํ•„๋“œ๊ฐ€ ๋Š˜๋ฉด
328
+ ํ”ฝ์Šค์ฒ˜๋Š” ๊ทธ๋Œ€๋กœ ์žˆ๊ณ  ์‹œํ—˜์€ ๋Š˜์–ด๋‚œ ์ž๋ฆฌ๋ฅผ ๋ณด์ง€ ๋ชปํ–ˆ๋‹ค.
329
+
330
+ ๋„ค ๋ฒˆ์งธ ๊ฒƒ์€ **ํ•œ ๊ฒน ์•ˆ**์ด๋ผ ์ตœ์ƒ์œ„๋งŒ ์„ธ๋˜ ์ฒซ ํŒ์ด ๋†“์ณค๋‹ค. ๊ทธ๋ž˜์„œ ํ”ฝ์Šค์ฒ˜์˜ ํ‚ค๋ฅผ
331
+ ๊นŠ์ด ์ƒ๊ด€์—†์ด ๋‹ค ๋ชจ์•„ ๋†“๊ณ  ๋งž๋Œ„๋‹ค.
332
+
333
+ ๊ทธ๋ž˜์„œ ํ”ฝ์Šค์ฒ˜๋ฅผ ๋ฏฟ์ง€ ์•Š๊ณ  **ํ˜•์‹์ด ์Šค์Šค๋กœ ๋ฐํžŒ ํ•„๋“œ ๋ชฉ๋ก**์„ ์ฝ๋Š”๋‹ค. ํ˜•์‹์ด ๋ถ™์€
334
+ ํƒ€์ž… ์„ ์–ธ(`dist/types.d.ts`)์ด ์ •๋ณธ์ด๊ณ , ๊ฑฐ๊ธฐ ์ ํžŒ ์ด๋ฆ„์ด ํ”ฝ์Šค์ฒ˜์— ์—†์œผ๋ฉด ์—ฌ๊ธฐ์„œ
335
+ ๊นจ์ง„๋‹ค. ๋‹ค์Œ ๋ฒˆ ํ•„๋“œ๋Š” ํ™”๋ฉด์ด ์•„๋‹ˆ๋ผ ์ด ์‹œํ—˜์ด ๋จผ์ € ์žก๋Š”๋‹ค.
336
+ */
337
+ describe('ํ”ฝ์Šค์ฒ˜๊ฐ€ ํ˜•์‹์„ ๋‹ค ๋‹ด๋Š”๋‹ค', () => {
338
+ /**
339
+ * ํ˜•์‹์ด ๋ถ™์—ฌ ๋ณด๋‚ธ ํƒ€์ž… ์„ ์–ธ ํŒŒ์ผ.
340
+ *
341
+ * `require.resolve` ๋กœ๋Š” ๋ชป ์žก๋Š”๋‹ค โ€” jest ์˜ resolver ๊ฐ€ `.d.ts` ๋ฅผ ๋ชจ๋“ˆ๋กœ ๋ณด์ง€
342
+ * ์•Š๋Š”๋‹ค. ์˜ฌ๋ผ๊ฐ€๋ฉด์„œ ์ฐพ๊ณ , ๋ชป ์ฐพ์œผ๋ฉด ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋›ฐ์ง€ ์•Š๊ณ  ์—ฌ๊ธฐ์„œ ๊นจ์ง„๋‹ค.
343
+ */
344
+ function declarationPath(): string {
345
+ const here = dirname(fileURLToPath(import.meta.url))
346
+ for (let at = here; at !== dirname(at); at = dirname(at)) {
347
+ const candidate = join(at, 'node_modules/@hatiolab/figure-model/dist/types.d.ts')
348
+ if (existsSync(candidate)) return candidate
349
+ }
350
+ throw new Error('figure-model ์˜ ํƒ€์ž… ์„ ์–ธ์„ ๋ชป ์ฐพ์•˜๋‹ค โ€” ์ด ์‹œํ—˜์€ ๊ทธ๊ฒƒ์ด ์ •๋ณธ์ด๋‹ค')
351
+ }
352
+
353
+ /** ๋ถ™์€ ํƒ€์ž… ์„ ์–ธ์—์„œ ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์˜ ํ•„๋“œ ์ด๋ฆ„์„ ๋ฝ‘๋Š”๋‹ค. */
354
+ function fieldsOf(name: string): string[] {
355
+ const declared = readFileSync(declarationPath(), 'utf-8')
356
+ const start = declared.indexOf(`export interface ${name} {`)
357
+ expect(start).toBeGreaterThan(-1)
358
+
359
+ // ์ค‘์ฒฉ์ด ์—†๋Š” ํ‰ํ‰ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋‹ค. ๋‹ค์Œ ๋‹ซ๋Š” ์ค‘๊ด„ํ˜ธ๊ฐ€ ๋์ด๋‹ค.
360
+ const body = declared.slice(start, declared.indexOf('\n}', start))
361
+ return [...body.matchAll(/^\s{4}(\w+)\??:/gm)].map(m => m[1])
362
+ }
363
+
364
+ /** ํ”ฝ์Šค์ฒ˜๊ฐ€ ์“ด ํ‚ค๋ฅผ ๊นŠ์ด ์ƒ๊ด€์—†์ด ๋ชจ์€๋‹ค โ€” ํ˜๋ฆฐ ํ•„๋“œ๊ฐ€ ํ•œ ๊ฒน ์•ˆ์— ์žˆ์—ˆ๋‹ค. */
365
+ function keysDeep(value: unknown, into = new Set<string>()): Set<string> {
366
+ if (Array.isArray(value)) value.forEach(one => keysDeep(one, into))
367
+ else if (value && typeof value === 'object') {
368
+ for (const [key, one] of Object.entries(value)) {
369
+ into.add(key)
370
+ keysDeep(one, into)
371
+ }
372
+ }
373
+ return into
374
+ }
375
+
376
+ const WRITTEN = keysDeep(FULL)
377
+
378
+ /*
379
+ `fold` ๊ฐ€ ์‹ค์ œ๋กœ ๋‚ฑ๊ฐœ๋กœ ํ‘ธ๋Š” ๊ฒƒ๋“ค์ด๋‹ค. ํ†ต์งธ๋กœ ๋‚˜๋ฅด๋Š” ๊ตฌ์กฐ(`transform` ์ฒ˜๋Ÿผ)๋Š” ํ•„๋“œ๊ฐ€
380
+ ๋Š˜์–ด๋„ ์•ˆ ์ƒˆ๋ฏ€๋กœ ์—ฌ๊ธฐ ์—†๋‹ค.
381
+ */
382
+ for (const name of ['FigureSource', 'FigurePart', 'PartShape', 'HollowSpec']) {
383
+ it(`${name} ๊ฐ€ ๊ฐ€์ง„ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ ์™•๋ณต ํ”ฝ์Šค์ฒ˜์— ์žˆ๋‹ค`, () => {
384
+ expect(fieldsOf(name).filter(field => !WRITTEN.has(field))).toEqual([])
385
+ })
386
+ }
387
+ })
388
+
389
+ /*
390
+ * โ”€โ”€ The placement is a height, not a flip โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
391
+ *
392
+ * `placement` says which reference surface a figure of this kind attaches to. It is a fact about
393
+ * the figure *type*, and what acts on it is the scene placing an instance: a floor figure stands on
394
+ * the ground, a ceiling one hangs at ceiling height less its own. `operato-scene`'s archetype says
395
+ * the same โ€” *ceiling: zPos derived from ceiling minus depth* (`archetypes.ts`).
396
+ *
397
+ * **Nothing about the authored shape depends on it.** The format measures every part from the box
398
+ * centre, so two figures with identical parts and different placements have identical `parts`.
399
+ *
400
+ * It was wired the other way for a while: the editing canvas fed the figure's placement to the
401
+ * model layer's `state.placement`, a scene-wide coordinate mode that flips what `zPos` names for
402
+ * everything in that layer. It stood in for the archetype because the modeller holds one figure โ€”
403
+ * and every number the author had typed then meant a different face depending on a dropdown, while
404
+ * what got saved never changed. These four tests hold that line.
405
+ */
406
+ test('๋ฐฐ์น˜๋ฅผ ๋ฐ”๊ฟ”๋„ ๋ถ€ํ’ˆ์˜ ์ž๋ฆฌ๋Š” ํ•œ ๊ธ€์ž๋„ ์•ˆ ๋ฐ”๋€๋‹ค', () => {
407
+ const source: FigureSource = {
408
+ type: 'OHT_TEST',
409
+ base: { x: 2, y: 3, z: 2 },
410
+ parts: [
411
+ /* ๋‘๊ป˜๊ฐ€ ์„œ๋กœ ๋‹ฌ๋ผ์•ผ ํ•œ๋‹ค โ€” ๋‘๊ป˜์— ๋น„๋ก€ํ•˜๋Š” ์–ด๊ธ‹๋‚จ์€ ํ†ต์งธ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ ํ˜•์ƒ์„ ๋ฒŒ๋ฆฐ๋‹ค. */
412
+ { name: 'rail', primitive: 'cube', transform: { position: { x: 0, y: 1.425, z: 0 }, size: { x: 2, y: 0.15, z: 0.4 } }, material: { token: 'palette.primary' } },
413
+ { name: 'belt', primitive: 'cube', transform: { position: { x: 0, y: 0, z: 0 }, size: { x: 0.1, y: 1, z: 0.1 } }, material: { token: 'palette.neutral' } }
414
+ ]
415
+ }
416
+
417
+ const authored = JSON.stringify(source.parts)
418
+
419
+ for (const placement of ['floor', 'ceiling', 'center'] as const) {
420
+ const { draft, parts } = fromFigureSource({ ...source, placement })
421
+ assert.equal(
422
+ JSON.stringify(toFigureSource(draft, parts).parts),
423
+ authored,
424
+ `${placement}: ๋ฐฐ์น˜๊ฐ€ ์ €์ž‘ํ•œ ์ž๋ฆฌ๋ฅผ ๋ฐ”๊ฟจ๋‹ค`
425
+ )
426
+ }
427
+ })
428
+
429
+ test('ํŽธ์ง‘ ์บ”๋ฒ„์Šค์˜ zPos ๋Š” ๋ฐฐ์น˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ฐ‘๋ฉด์ด๋‹ค', () => {
430
+ /*
431
+ ์ €์ž‘์ž๊ฐ€ ์ธ์ŠคํŽ™ํ„ฐ์— ์ ๋Š” ์ˆ˜๋‹ค. ๊ทธ ๋œป์ด ๋“œ๋กญ๋‹ค์šด์— ๋”ฐ๋ผ ๋ฐ‘๋ฉดโ†”์œ—๋ฉด์œผ๋กœ ๋ฐ”๋€Œ๋ฉด, ์•„๋ฌด๊ฒƒ๋„
432
+ ์•ˆ ๊ณ ์ณค๋Š”๋ฐ ๋ชจ๋“  ๋ถ€ํ’ˆ์˜ ๋†’์ด๊ฐ€ ๋‹ฌ๋ผ์ง„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค. OHT ์—์„œ ์‹ค์ œ๋กœ ๊ทธ๋žฌ๋‹ค โ€”
433
+ overhead-rail ์ด 1.35 ์—์„œ -1.5 ๊ฐ€ ๋˜์—ˆ๋‹ค.
434
+ */
435
+ const part: PartModel = { type: 'figure-part', name: 'rail', primitive: 'cube', left: 0, top: 0, width: 2, height: 0.4, zPos: 1.35, depth: 0.15 }
436
+ const draft: FigureDraft = { width: 2, height: 2, depth: 3 }
437
+
438
+ for (const placement of [undefined, 'floor', 'ceiling', 'center'] as const) {
439
+ const written = partToFigure(part, { ...draft, placement })
440
+ /* ๋ฐ‘๋ฉด 1.35 + ๋‘๊ป˜ 0.15/2 = ์ค‘์‹ฌ 1.425. ์ƒ์ž ๋†’์ด 3 ์˜ ์ ˆ๋ฐ˜์„ ๋นผ๋ฉด -0.075. */
441
+ assert.equal(written.transform.position.y, -0.075, `${placement}: ์ ‘๋Š” ์ชฝ์ด ๋ฐฐ์น˜๋ฅผ ๋ดค๋‹ค`)
442
+ assert.equal(partFromFigure(written, { ...draft, placement }).zPos, 1.35, `${placement}: ํŽด๋Š” ์ชฝ์ด ๋ฐฐ์น˜๋ฅผ ๋ดค๋‹ค`)
443
+ }
444
+ })
445
+
446
+ describe('๋ฐฐ์น˜๋Š” ์”ฌ์˜ ์ขŒํ‘œ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ˆ๋‹ค', () => {
447
+ const read = (rel: string) => readFileSync(join(dirname(fileURLToPath(import.meta.url)), rel), 'utf8')
448
+
449
+ /*
450
+ ์†Œ์Šค๋ฅผ ์ฝ๋Š” ๊ฒ€์‚ฌ๋‹ค. Lit ์ปดํฌ๋„ŒํŠธ๊ฐ€ document ์—†์ด ์•ˆ ๋œจ๋Š” ์ž๋ฆฌ๋ผ ๊ทธ๋ ‡๊ณ , ๋ฌด์—‡๋ณด๋‹ค
451
+ **์—†์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์ด ์—†๋Š”์ง€**๋ฅผ ๋ฌป๋Š” ๊ฒ€์‚ฌ๋Š” ํ˜ธ์ถœ๋กœ๋Š” ๋ฌผ์„ ์ˆ˜๊ฐ€ ์—†๋‹ค.
452
+ */
453
+ it('๋ณ€ํ™˜์ด ์ขŒํ‘œ ๋ชจ๋“œ๋ฅผ ์•„์˜ˆ ๋ชจ๋ฅธ๋‹ค', () => {
454
+ const source = read('../client/modeller/figure-source.ts')
455
+
456
+ assert.doesNotMatch(source, /zPosOffset|toScenePlacement/, '๋ณ€ํ™˜์— ์ขŒํ‘œ ๋ชจ๋“œ๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์™”๋‹ค')
457
+ assert.doesNotMatch(
458
+ source,
459
+ /placement.*=>.*'inverted'|'inverted'.*:/,
460
+ '์”ฌ์˜ ์ขŒํ‘œ ๋‚ฑ๋ง์ด ๋ณ€ํ™˜์— ๋“ค์–ด์™”๋‹ค'
461
+ )
462
+ })
463
+
464
+ it('ํŽธ์ง‘ ์บ”๋ฒ„์Šค๊ฐ€ ์”ฌ์— ๋ฐฐ์น˜๋ฅผ ์•ˆ ์‹ฃ๋Š”๋‹ค', () => {
465
+ const source = read('../client/modeller/figure-canvas.ts')
466
+
467
+ assert.doesNotMatch(
468
+ source,
469
+ /placement:/,
470
+ 'ํŽธ์ง‘ ์บ”๋ฒ„์Šค๊ฐ€ ๋ฐฐ์น˜๋ฅผ ์”ฌ์— ์‹ค์œผ๋ฉด ์ €์ž‘์ž๊ฐ€ ์ ์€ ๋†’์ด์˜ ๋œป์ด ๋“œ๋กญ๋‹ค์šด๋งˆ๋‹ค ๋ฐ”๋€๋‹ค'
471
+ )
472
+ })
473
+
474
+ it('setPlacement ๊ฐ€ ๋ถ€ํ’ˆ์„ ์•ˆ ๊ฑด๋“œ๋ฆฐ๋‹ค', () => {
475
+ const source = read('../client/modeller/figure-inspector.ts')
476
+ const body = source.slice(source.indexOf('private setPlacement'), source.indexOf('private setPlacement') + 500)
477
+
478
+ assert.match(body, /detail: \{ draft \}/, '๋ฐฐ์น˜๋Š” ํ•„๋“œ ํ•˜๋‚˜๋งŒ ์“ด๋‹ค')
479
+ assert.doesNotMatch(body, /zPos/, '๋ฐฐ์น˜๋ฅผ ๋ฐ”๊พธ๋ฉด์„œ ๋ถ€ํ’ˆ ๋†’์ด๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜๋ฉด ์•ˆ ๋œ๋‹ค')
480
+ })
481
+ })
482
+
483
+ describe('๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ๋†’์ด๋ฅผ ์”ฌ์—๊ฒŒ ๋งก๊ธด๋‹ค', () => {
484
+ const read = (rel: string) => readFileSync(join(dirname(fileURLToPath(import.meta.url)), rel), 'utf8')
485
+ const preview = read('../client/modeller/figure-preview.ts')
486
+
487
+ /*
488
+ ๋ฐฐ์น˜ ๊ธฐ์ค€์„ ๋†’์ด๋กœ ์˜ฎ๊ธฐ๋Š” ๊ทœ์น™์€ ์”ฌ์ด ๊ฐ–๋Š”๋‹ค โ€” `FigureRealObject.effectiveZPos` ๊ฐ€ ์ฒญ์‚ฌ์ง„์˜
489
+ `placement` ์™€ ๋ ˆ์ด์–ด์˜ `heights` ๋กœ ์ •ํ•œ๋‹ค(ADR-0045). ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ใ€Œ๋ณด๋“œ์— ๋†“์•˜์„ ๋•Œใ€๋ฅผ
490
+ ๋งํ•˜๋Š” ์ž๋ฆฌ์ด๋ฏ€๋กœ ๋ณด๋“œ์™€ ๊ฐ™์€ ๊ธธ๋กœ ์„ธ์›Œ์•ผ ํ•œ๋‹ค.
491
+
492
+ ์ด ํ™”๋ฉด์ด ๊ทœ์น™์„ ํ•œ ๋ฒŒ ๋” ๋“ค๊ณ  ์žˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์”ฌ์˜ ๊ทœ์น™์ด ๋ฐ”๋€Œ์–ด๋„ ์ด ํ™”๋ฉด๋งŒ ์˜› ๋‹ต์„ ๋‚ด๊ณ ,
493
+ ์ €์ž‘์ž๋Š” ๋ณด๋“œ์— ๋†“๊ณ  ๋‚˜์„œ์•ผ ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ์„ ์•ˆ๋‹ค.
494
+ */
495
+ it('์ธ์Šคํ„ด์Šค์— zPos ๋ฅผ ์ ์ง€ ์•Š๋Š”๋‹ค โ€” ์ ์œผ๋ฉด ์ ์€ ๊ฐ’์ด ์ด๊ธด๋‹ค', () => {
496
+ const at = preview.indexOf('const components = PREVIEW_WAYS.map')
497
+ const built = preview.slice(at, preview.indexOf('const draft = {', at))
498
+
499
+ assert.doesNotMatch(built, /zPos/, '์—ฌ๊ธฐ์„œ ๋†’์ด๋ฅผ ์ •ํ•˜๋ฉด ์”ฌ์ด ๋ฌด์—‡์„ ํ• ์ง€๊ฐ€ ์ด ํ™”๋ฉด์—์„œ๋งŒ ์•ˆ ๋ณด์ธ๋‹ค')
500
+ })
501
+
502
+ it('๋ฐฐ์น˜๋ฅผ ๋†’์ด๋กœ ์˜ฎ๊ธฐ๋Š” ๊ณ„์‚ฐ์„ ๋“ค๊ณ  ์žˆ์ง€ ์•Š๋‹ค', () => {
503
+ assert.doesNotMatch(preview, /bottomOf|zPosOf/, '๊ทœ์น™์ด ๋‘ ๋ฒŒ์ด๋ฉด ๊ฐˆ๋ฆฐ๋‹ค')
504
+ })
505
+
506
+ it('์ฒœ์žฅ ๋†’์ด๋ฅผ ์”ฌ์— ์‹ค์–ด ์ค€๋‹ค โ€” ๊ฐ’์ด ์‚ฌ๋Š” ์ž๋ฆฌ๊ฐ€ ๊ฑฐ๊ธฐ๋‹ค', () => {
507
+ assert.match(preview, /heights: \{ floor: 0, ceiling: view\.ceilingHeight \}/)
508
+ })
509
+
510
+ it('๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ขŒํ‘œ ๋ชจ๋“œ๋ฅผ ๋ณด๊ธฐ ์„ค์ •์—์„œ ๋ฐ›๋Š”๋‹ค โ€” ๋„ํ˜•์—์„œ๊ฐ€ ์•„๋‹ˆ๋ผ', () => {
511
+ assert.match(preview, /placement: view\.placement/, '๋ณด๊ธฐ ์„ค์ •์˜ ์ขŒํ‘œ ๋ชจ๋“œ๋ฅผ ์”ฌ์— ์‹ค์–ด์•ผ ํ•œ๋‹ค')
512
+ assert.doesNotMatch(preview, /placement: (source|toScenePlacement)/, '๋„ํ˜•์˜ ๋ฐฐ์น˜๋ฅผ ์”ฌ ๋ชจ๋“œ๋กœ ์“ฐ๋ฉด ์•ˆ ๋œ๋‹ค')
513
+ })
514
+
515
+ it('๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ๋ณด๊ธฐ ์„ค์ •์˜ ์กฐ๋ช…ยทํ™˜๊ฒฝ์„ ๋”ฐ๋ฅธ๋‹ค', () => {
516
+ assert.match(preview, /\.\.\.sceneLook\(view\)/, 'ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ํŽธ์ง‘ ์บ”๋ฒ„์Šค์™€ ๋‘ ํ™”๋ฉด์ด ๊ฐˆ๋ฆฐ๋‹ค')
517
+ assert.doesNotMatch(preview, /environment: 'studio'/, 'ํ™˜๊ฒฝ์ด ํ•˜๋“œ์ฝ”๋”ฉ์œผ๋กœ ๋‚จ์•„ ์žˆ๋‹ค')
518
+ })
519
+ })
520
+
521
+ describe('๋ณด๊ธฐ ์„ค์ •์ด ํŽธ์ง‘ ์บ”๋ฒ„์Šค๋กœ ์ƒˆ์ง€ ์•Š๋Š”๋‹ค', () => {
522
+ const read = (rel: string) => readFileSync(join(dirname(fileURLToPath(import.meta.url)), rel), 'utf8')
523
+
524
+ it('sceneLook ์ด ์ขŒํ‘œ ๋ชจ๋“œ์™€ ์ฒœ์žฅ ๋†’์ด๋ฅผ ๋บ€๋‹ค', () => {
525
+ const view = read('../client/modeller/figure-view.ts')
526
+ assert.match(view, /const \{ placement, ceilingHeight, \.\.\.look \} = view/)
527
+ })
528
+
529
+ it('ํŽธ์ง‘ ์บ”๋ฒ„์Šค๊ฐ€ rest spread ๋กœ ๋ณด๊ธฐ ์„ค์ •์„ ๋„˜๊ธฐ์ง€ ์•Š๋Š”๋‹ค', () => {
530
+ /*
531
+ ํ•œ๋™์•ˆ `const { grid, ..., ...light } = view` ์˜€๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์„ค์ •์„ ํ•˜๋‚˜ ๋”ํ•  ๋•Œ๋งˆ๋‹ค
532
+ ๊ทธ๊ฒƒ์ด ๋‘ ํ™”๋ฉด์— **๋ง์—†์ด** ์‹ค๋ฆฐ๋‹ค โ€” ์ขŒํ‘œ ๋ชจ๋“œ๊ฐ€ ๋ฐ”๋กœ ๊ทธ ๊ธธ๋กœ ๋“ค์–ด๊ฐˆ ๋ป”ํ–ˆ๋‹ค.
533
+ */
534
+ const canvas = read('../client/modeller/figure-canvas.ts')
535
+ const fn = canvas.slice(canvas.indexOf('private sceneView'), canvas.indexOf('private sceneView') + 200)
536
+
537
+ assert.match(fn, /sceneLook\(/)
538
+ assert.doesNotMatch(fn, /\.\.\.light/, '๋‚˜๋จธ์ง€๋ฅผ ํ†ต์งธ๋กœ ๋„˜๊ธฐ๋ฉด ์ƒˆ ์„ค์ •์ด ์กฐ์šฉํžˆ ์‹ค๋ฆฐ๋‹ค')
539
+ })
540
+ })
541
+
542
+ /*
543
+ * โ”€โ”€ ๋ง‰๋Š” ๊ฒƒ๊ณผ ์•Œ๋ฆฌ๋Š” ๊ฒƒ์„ ํ™”๋ฉด์ด ๊ฐˆ๋ผ์•ผ ํ•œ๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
544
+ *
545
+ * ํŒ์ • ํŒจ๋„์€ ์œ„๋ฐ˜์„ ์ „๋ถ€ ๊ฐ™์€ `info` ๋กœ ๊ทธ๋ ธ๋‹ค. ๊ธฐ์ค€ ์ƒ์ž ์œ„๋ฐ˜ ๋‘˜์ด ๋ฐœํ–‰์„ ๋ง‰๊ฒŒ ๋œ ๋’ค์—๋„
546
+ * ๊ทธ๋Œ€๋กœ๋ผ, ์ €์ž‘์ž๋Š” ๋‹ค์„ฏ ์ค„์˜ ์•Œ๋ฆผ์„ ๋ณด๊ณ  **๋ฐœํ–‰์„ ๋ˆŒ๋Ÿฌ ๋ณด๊ณ  ๋‚˜์„œ์•ผ** ๋ง‰ํžŒ ๊ฒƒ์„ ์•ˆ๋‹ค.
547
+ *
548
+ * ๋“ฑ๊ธ‰์€ ํ˜•์‹์ด ์•ˆ๋‹ค(`blocksRelease`). ํ™”๋ฉด์ด ์ œ ๋ชฉ๋ก์„ ๋“ค๋ฉด ์ฝ”๋“œ๊ฐ€ ํ•˜๋‚˜ ๋Š˜ ๋•Œ ์กฐ์šฉํžˆ
549
+ * ใ€Œ์•Œ๋ฆผใ€ ์ชฝ์œผ๋กœ ๋–จ์–ด์ง„๋‹ค โ€” ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์ชฝ์œผ๋กœ ๊ธฐ์šฐ๋Š” ๊ธฐ๋ณธ๊ฐ’์ด๋‹ค.
550
+ */
551
+ describe('ํŒ์ • ํŒจ๋„์ด ๋ฐœํ–‰์„ ๋ง‰๋Š” ๊ฒƒ์„ ๊ฐˆ๋ผ ๋ณด์ธ๋‹ค', () => {
552
+ const read = (rel: string) => readFileSync(join(dirname(fileURLToPath(import.meta.url)), rel), 'utf8')
553
+ const report = read('../client/modeller/figure-report.ts')
554
+
555
+ it('๋“ฑ๊ธ‰์„ ํ˜•์‹์—์„œ ๋ฐ›๋Š”๋‹ค โ€” ์ œ ๋ชฉ๋ก์„ ๋“ค์ง€ ์•Š๋Š”๋‹ค', () => {
556
+ assert.match(report, /blocksRelease/, 'ํ˜•์‹์ด ์–ด๋А ์œ„๋ฐ˜์ด ๋ง‰๋Š”์ง€ ์•ˆ๋‹ค')
557
+ assert.doesNotMatch(
558
+ report,
559
+ /'part-outside-base'|'parts-off-placement-face'/,
560
+ 'ํ™”๋ฉด์ด ์ฝ”๋“œ ์ด๋ฆ„์„ ์ ์œผ๋ฉด ํ˜•์‹๊ณผ ๋‘ ๋ฒŒ์ด ๋œ๋‹ค'
561
+ )
562
+ })
563
+
564
+ it('๋ง‰๋Š” ๊ฒƒ์€ ๋‹ค๋ฅธ ์•„์ด์ฝ˜์œผ๋กœ ๊ทธ๋ฆฐ๋‹ค', () => {
565
+ assert.match(report, /v\.blocking \? 'block' : 'info'/, '๊ฐ™์€ ์•„์ด์ฝ˜์ด๋ฉด ๊ฐˆ๋ฆฐ ๋œป์ด ํ™”๋ฉด์— ์•ˆ ๋‚˜์˜จ๋‹ค')
566
+ })
567
+
568
+ it('์–ผ๋งˆ๋‚˜ ๋„˜์ณค๋Š”์ง€ ๋ณด์—ฌ ์ค€๋‹ค', () => {
569
+ /* ใ€Œ์ƒ์ž๋ฅผ ๋ฒ—์–ด๋‚œ๋‹คใ€๋งŒ์œผ๋กœ๋Š” 2mm ์ธ์ง€ 250mm ์ธ์ง€ ๋ชจ๋ฅด๊ณ , ๊ทธ ๋‘˜์€ ๊ณ ์น˜๋Š” ํ’ˆ์ด ๋‹ค๋ฅด๋‹ค. */
570
+ assert.match(report, /v\.actual/, '๋„˜์นœ ์–‘์„ ๋ฒ„๋ฆฌ๋ฉด ์ €์ž‘์ž๊ฐ€ ๋ฌด์—‡๋ถ€ํ„ฐ ๊ณ ์น ์ง€ ๋ชป ๊ณ ๋ฅธ๋‹ค')
571
+ })
572
+ })
@@ -6,7 +6,7 @@
6
6
  * ์—ฌ๋Ÿฟ์ด๋ฉด ๋งˆ์ง€๋ง‰์ด ์ด๊ธฐ๊ณ  ์•„๋ฌด ๊ฒฝ๊ณ ๋„ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ๋‚ด ํŒŒ์ผ์„ ์ฝ์–ด๋„ ํ™”๋ฉด์— ๋œจ๋Š” ๊ฐ’์„ ์•Œ ์ˆ˜
7
7
  * ์—†๋‹ค โ€” ๋‚ด๊ฐ€ ์ ์€ ๊ฐ’์ด ์•„๋‹ˆ๋ผ ์ด๊ธด ๊ฐ’์ด ๋œฌ๋‹ค.
8
8
  *
9
- * ์‹ค์ œ๋กœ ๊ทธ๋žฌ๋‹ค. figure ์•ฑ์˜ ๋ฐœํ–‰ ๋‹จ์ถ”๊ฐ€ ใ€Œ๋ฐฐํฌใ€๋กœ ๋–ด๋‹ค(board-ui ์˜ `button.release` ๊ฐ€ ์ด๊ฒผ๋‹ค).
9
+ * ์‹ค์ œ๋กœ ๊ทธ๋žฌ๋‹ค. figure ์•ฑ์˜ ๋ฐœํ–‰ ๋‹จ์ถ”๊ฐ€ ใ€Œ๋ฐฐํฌใ€๋กœ ๋–ด๋‹ค(draft-ui ์˜ `button.release` ๊ฐ€ ์ด๊ฒผ๋‹ค).
10
10
  * ๊ทธ๋•Œ figure ์˜ 266๊ฐœ ํ‚ค ์ค‘ ์•„ํ™‰์ด **๋‹ค๋ฅธ ๋œป์œผ๋กœ** ๊ฒน์ณ ์žˆ์—ˆ๋‹ค โ€” ๋ชจ๋ธ๋Ÿฌ๊ฐ€ ์ž๊ธฐ ๋ถ€ํ’ˆ์„ board ์˜
11
11
  * ๋‚ฑ๋ง์ธ ใ€Œํ’ˆ๋ชฉใ€์ด๋ผ ๋ถ€๋ฅด๊ณ  ์žˆ์—ˆ๊ณ  ์•„๋ฌด๊ฒƒ๋„ ๊ทธ๊ฒƒ์„ ๋งํ•ด ์ฃผ์ง€ ์•Š์•˜๋‹ค.
12
12
  *
@@ -28,7 +28,7 @@ import { readFileSync, readdirSync } from 'node:fs'
28
28
  import { fileURLToPath } from 'node:url'
29
29
  import { join } from 'node:path'
30
30
 
31
- import { CHANNEL_PATHS, CLIP_DRIVES, INTERPOLATIONS } from '@hatiolab/figure-model'
31
+ import { CHANNEL_PATHS, INTERPOLATIONS } from '@hatiolab/figure-model'
32
32
 
33
33
  const HERE = fileURLToPath(new URL('.', import.meta.url))
34
34
  const TRANSLATIONS = join(HERE, '../translations')
@@ -125,23 +125,22 @@ test('โ˜… ํ†ต์ด ๊ฐˆ๋ฆฐ ์ง โ€” ์‚ฌ๋‹ค๋ฆฌ ์ด๋ฆ„๊ณผ ์„ค๋ช…', () => {
125
125
  /*
126
126
  ์• ๋‹ˆ๋ฉ”์ด์…˜ ํƒญ๋„ ์—ด๊ฑฐ ๊ฐ’์„ ํ‚ค์— ์ด์–ด ๋ถ™์ธ๋‹ค. ์‚ฌ๋‹ค๋ฆฌ์™€ **๊ฐ™์€ ๋ชจ์–‘์˜ ํ•จ์ •**์ด๋ผ ๊ฐ™์€ ๋ชป์„ ๋ฐ•๋Š”๋‹ค.
127
127
 
128
- ๊ฐˆ๋ฆฐ ํ†ต์ด ๋‘˜์ด๋‹ค โ€” ๊ตฌ๋™์€ ์ด๋ฆ„(`label`)๊ณผ ์„ค๋ช…(`text.โ€ฆ-note`), ๊ฒฝ๋กœ๋Š” ์ด๋ฆ„(`label`)๊ณผ
129
- ๋‹จ์œ„ ์„ค๋ช…(`text.โ€ฆ-unit`). ํ•œ์ชฝ๋งŒ ๋„ฃ์œผ๋ฉด ํ™”๋ฉด์— ํ‚ค๊ฐ€ ๊ทธ๋Œ€๋กœ ๋œจ๊ณ , ๋„“์€ ์•ž๋จธ๋ฆฌ๊ฐ€ ์œ„์˜
130
- ๋ฏธ์‚ฌ์šฉ ๊ฒ€์‚ฌ๋ฅผ ๋ฎ์–ด ํ†ต๊ณผ์‹œํ‚จ๋‹ค.
128
+ ๊ฒฝ๋กœ๋Š” ์ด๋ฆ„(`label`)๊ณผ ๋‹จ์œ„ ์„ค๋ช…(`text.โ€ฆ-unit`)์œผ๋กœ ๊ฐˆ๋ฆฐ๋‹ค. ํ•œ์ชฝ๋งŒ ๋„ฃ์œผ๋ฉด ํ™”๋ฉด์— ํ‚ค๊ฐ€
129
+ ๊ทธ๋Œ€๋กœ ๋œจ๊ณ , ๋„“์€ ์•ž๋จธ๋ฆฌ๊ฐ€ ์œ„์˜ ๋ฏธ์‚ฌ์šฉ ๊ฒ€์‚ฌ๋ฅผ ๋ฎ์–ด ํ†ต๊ณผ์‹œํ‚จ๋‹ค.
130
+
131
+ `CLIP_DRIVES` ๋Š” ์—ฌ๊ธฐ ์—†๋‹ค. ๊ตฌ๋™ ๊ฐˆ๋ž˜๋Š” ํ™”๋ฉด์—์„œ ๊ณ ๋ฅด๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ **์–ด๋А ๋ชฉ๋ก์—
132
+ ์ ์—ˆ๋Š”๊ฐ€**๊ฐ€ ๋˜์—ˆ๋‹ค โ€” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋ฉด ์‹œ๊ฐ์ด ํ๋ฅด๊ณ  ํŒŒ๋ผ๋ฏธํ„ฐ๋ฉด ๊ฐ’์ด ์ž์„ธ๋ฅผ ๋งŒ๋“ ๋‹ค
133
+ (ADR-0051 โ‘ ).
131
134
 
132
135
  ์—ด๊ฑฐ ๊ฐ’์„ ์—ฌ๊ธฐ ๋‹ค์‹œ ์ ์ง€ ์•Š๊ณ  ํ˜•์‹์—์„œ ์ฝ๋Š”๋‹ค. ์ ์–ด ๋‘๋ฉด ํ˜•์‹์— ๊ฐ’์„ ํ•˜๋‚˜ ๋”ํ•  ๋•Œ
133
136
  ๊ฒ€์‚ฌ๊ฐ€ ์กฐ์šฉํžˆ ํ†ต๊ณผํ•œ๋‹ค.
134
137
  */
135
- test('โ˜… ์—ด๊ฑฐ ๊ฐ’๋งˆ๋‹ค ์ง์ด ๋‹ค ์žˆ๋‹ค โ€” ๊ตฌ๋™ ยท ๊ฒฝ๋กœ ยท ๋ณด๊ฐ„', () => {
138
+ test('โ˜… ์—ด๊ฑฐ ๊ฐ’๋งˆ๋‹ค ์ง์ด ๋‹ค ์žˆ๋‹ค โ€” ๊ฒฝ๋กœ ยท ๋ณด๊ฐ„', () => {
136
139
  const missing: string[] = []
137
140
  const want = (key: string) => {
138
141
  if (!(key in KO)) missing.push(key)
139
142
  }
140
143
 
141
- for (const drive of CLIP_DRIVES) {
142
- want(`figure.label.drive-${drive}`)
143
- want(`figure.text.drive-${drive}-note`)
144
- }
145
144
  for (const path of CHANNEL_PATHS) {
146
145
  want(`figure.label.path-${path}`)
147
146
  want(`figure.text.path-${path}-unit`)
@@ -156,8 +155,6 @@ test('โ˜… ์—ด๊ฑฐ ๊ฐ’๋งˆ๋‹ค ์ง์ด ๋‹ค ์žˆ๋‹ค โ€” ๊ตฌ๋™ ยท ๊ฒฝ๋กœ ยท ๋ณด๊ฐ„', ()
156
155
  test('โ˜… ์• ๋‹ˆ๋ฉ”์ด์…˜ ํƒญ์ด ๊ทธ ํ†ต์—์„œ ์ฐพ๋Š”๋‹ค โ€” ์ด๋ฆ„์€ label, ์„ค๋ช…์€ text', () => {
157
156
  const source = readFileSync(join(CLIENT, 'modeller/figure-animations.ts'), 'utf-8')
158
157
 
159
- assert.ok(source.includes("'figure.label.drive-' + drive"), '๊ตฌ๋™ ์ด๋ฆ„์„ label ํ†ต์—์„œ ์ฐพ์ง€ ์•Š๋Š”๋‹ค')
160
- assert.ok(source.includes("'figure.text.drive-'"), '๊ตฌ๋™ ์„ค๋ช…์„ text ํ†ต์—์„œ ์ฐพ์ง€ ์•Š๋Š”๋‹ค')
161
158
  assert.ok(source.includes("'figure.label.path-' + path"), '๊ฒฝ๋กœ ์ด๋ฆ„์„ label ํ†ต์—์„œ ์ฐพ์ง€ ์•Š๋Š”๋‹ค')
162
159
  assert.ok(source.includes("'figure.text.path-' + channel.path + '-unit'"), '๋‹จ์œ„ ์„ค๋ช…์„ text ํ†ต์—์„œ ์ฐพ์ง€ ์•Š๋Š”๋‹ค')
163
160
  })