@things-factory/figure-ui 10.1.65 → 10.1.67

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 (176) hide show
  1. package/client/graphql/index.ts +0 -45
  2. package/client/modeller/figure-history-panel.ts +6 -145
  3. package/client/modeller/figure-v3-capability.ts +2 -2
  4. package/client/modeller/figure-v3-clips.ts +2 -2
  5. package/client/modeller/figure-v3-controls.ts +2 -2
  6. package/client/modeller/figure-v3-first-save.ts +1 -1
  7. package/client/modeller/figure-v3-links.ts +6 -6
  8. package/client/modeller/figure-v3-material.ts +11 -11
  9. package/client/modeller/figure-v3-parts.ts +7 -10
  10. package/client/modeller/figure-v3-preview.ts +3 -3
  11. package/client/modeller/figure-v3-properties.ts +5 -6
  12. package/client/modeller/figure-v3-verdict.ts +9 -10
  13. package/client/modeller/figure-view.ts +2 -3
  14. package/client/modeller/modeller-route.ts +2 -3
  15. package/client/modeller/scene-budget.ts +0 -14
  16. package/client/modeller/side-panel-styles.ts +5 -5
  17. package/client/modeller/v3-align.ts +16 -6
  18. package/client/modeller/v3-canvas.ts +12 -12
  19. package/client/modeller/v3-clips.ts +1 -1
  20. package/client/modeller/v3-controls.ts +1 -1
  21. package/client/modeller/v3-editor.ts +21 -21
  22. package/client/modeller/v3-fields.ts +1 -1
  23. package/client/modeller/v3-figure-facts.ts +1 -1
  24. package/client/modeller/v3-first-save.ts +3 -4
  25. package/client/modeller/v3-gizmo.ts +5 -5
  26. package/client/modeller/v3-help.ts +1 -1
  27. package/client/modeller/v3-joint-fan.ts +4 -5
  28. package/client/modeller/v3-keys.ts +1 -1
  29. package/client/modeller/v3-links.ts +9 -12
  30. package/client/modeller/v3-maturity.ts +22 -26
  31. package/client/modeller/v3-refusals.ts +17 -0
  32. package/client/modeller/v3-renderer.ts +1 -1
  33. package/client/modeller/v3-stage-model.ts +2 -2
  34. package/client/modeller/v3-thumbnail.ts +1 -3
  35. package/client/pages/figure-v3-modeller-page.ts +8 -11
  36. package/client/types.ts +2 -31
  37. package/dist-client/graphql/index.js +0 -45
  38. package/dist-client/graphql/index.js.map +1 -1
  39. package/dist-client/modeller/figure-history-panel.d.ts +4 -37
  40. package/dist-client/modeller/figure-history-panel.js +8 -154
  41. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  42. package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
  43. package/dist-client/modeller/figure-v3-capability.js +1 -1
  44. package/dist-client/modeller/figure-v3-capability.js.map +1 -1
  45. package/dist-client/modeller/figure-v3-clips.d.ts +2 -2
  46. package/dist-client/modeller/figure-v3-clips.js +2 -2
  47. package/dist-client/modeller/figure-v3-clips.js.map +1 -1
  48. package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
  49. package/dist-client/modeller/figure-v3-controls.js +2 -2
  50. package/dist-client/modeller/figure-v3-controls.js.map +1 -1
  51. package/dist-client/modeller/figure-v3-first-save.d.ts +1 -1
  52. package/dist-client/modeller/figure-v3-first-save.js +1 -1
  53. package/dist-client/modeller/figure-v3-first-save.js.map +1 -1
  54. package/dist-client/modeller/figure-v3-links.d.ts +5 -5
  55. package/dist-client/modeller/figure-v3-links.js +6 -6
  56. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  57. package/dist-client/modeller/figure-v3-material.d.ts +6 -6
  58. package/dist-client/modeller/figure-v3-material.js +11 -11
  59. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  60. package/dist-client/modeller/figure-v3-parts.d.ts +6 -8
  61. package/dist-client/modeller/figure-v3-parts.js +1 -1
  62. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  63. package/dist-client/modeller/figure-v3-preview.js.map +1 -1
  64. package/dist-client/modeller/figure-v3-properties.d.ts +4 -5
  65. package/dist-client/modeller/figure-v3-properties.js +5 -6
  66. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  67. package/dist-client/modeller/figure-v3-verdict.d.ts +2 -2
  68. package/dist-client/modeller/figure-v3-verdict.js +9 -10
  69. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  70. package/dist-client/modeller/figure-view.d.ts +2 -3
  71. package/dist-client/modeller/figure-view.js.map +1 -1
  72. package/dist-client/modeller/modeller-route.d.ts +2 -3
  73. package/dist-client/modeller/modeller-route.js +2 -3
  74. package/dist-client/modeller/modeller-route.js.map +1 -1
  75. package/dist-client/modeller/scene-budget.d.ts +0 -11
  76. package/dist-client/modeller/scene-budget.js +0 -12
  77. package/dist-client/modeller/scene-budget.js.map +1 -1
  78. package/dist-client/modeller/side-panel-styles.d.ts +2 -2
  79. package/dist-client/modeller/side-panel-styles.js +5 -5
  80. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  81. package/dist-client/modeller/v3-align.d.ts +9 -5
  82. package/dist-client/modeller/v3-align.js +13 -3
  83. package/dist-client/modeller/v3-align.js.map +1 -1
  84. package/dist-client/modeller/v3-canvas.d.ts +4 -4
  85. package/dist-client/modeller/v3-canvas.js +9 -9
  86. package/dist-client/modeller/v3-canvas.js.map +1 -1
  87. package/dist-client/modeller/v3-clips.js.map +1 -1
  88. package/dist-client/modeller/v3-controls.js.map +1 -1
  89. package/dist-client/modeller/v3-editor.d.ts +13 -13
  90. package/dist-client/modeller/v3-editor.js +19 -18
  91. package/dist-client/modeller/v3-editor.js.map +1 -1
  92. package/dist-client/modeller/v3-fields.js.map +1 -1
  93. package/dist-client/modeller/v3-figure-facts.d.ts +1 -1
  94. package/dist-client/modeller/v3-figure-facts.js +1 -1
  95. package/dist-client/modeller/v3-figure-facts.js.map +1 -1
  96. package/dist-client/modeller/v3-first-save.js +3 -4
  97. package/dist-client/modeller/v3-first-save.js.map +1 -1
  98. package/dist-client/modeller/v3-gizmo.d.ts +5 -5
  99. package/dist-client/modeller/v3-gizmo.js +1 -1
  100. package/dist-client/modeller/v3-gizmo.js.map +1 -1
  101. package/dist-client/modeller/v3-help.d.ts +1 -1
  102. package/dist-client/modeller/v3-help.js +1 -1
  103. package/dist-client/modeller/v3-help.js.map +1 -1
  104. package/dist-client/modeller/v3-joint-fan.js +4 -5
  105. package/dist-client/modeller/v3-joint-fan.js.map +1 -1
  106. package/dist-client/modeller/v3-keys.js.map +1 -1
  107. package/dist-client/modeller/v3-links.d.ts +5 -7
  108. package/dist-client/modeller/v3-links.js +6 -8
  109. package/dist-client/modeller/v3-links.js.map +1 -1
  110. package/dist-client/modeller/v3-maturity.d.ts +7 -7
  111. package/dist-client/modeller/v3-maturity.js +20 -24
  112. package/dist-client/modeller/v3-maturity.js.map +1 -1
  113. package/dist-client/modeller/v3-refusals.d.ts +6 -0
  114. package/dist-client/modeller/v3-refusals.js +18 -0
  115. package/dist-client/modeller/v3-refusals.js.map +1 -1
  116. package/dist-client/modeller/v3-renderer.js +1 -1
  117. package/dist-client/modeller/v3-renderer.js.map +1 -1
  118. package/dist-client/modeller/v3-stage-model.js.map +1 -1
  119. package/dist-client/modeller/v3-thumbnail.js +1 -3
  120. package/dist-client/modeller/v3-thumbnail.js.map +1 -1
  121. package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -5
  122. package/dist-client/pages/figure-v3-modeller-page.js +8 -11
  123. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  124. package/dist-client/route.d.ts +1 -1
  125. package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
  126. package/dist-client/tsconfig.tsbuildinfo +1 -1
  127. package/dist-client/types.d.ts +2 -29
  128. package/dist-client/types.js +3 -0
  129. package/dist-client/types.js.map +1 -1
  130. package/docs/v3-product-cutover.md +14 -24
  131. package/package.json +5 -5
  132. package/test/browser/modeller-page.js +6 -7
  133. package/test/browser/modeller.check.mjs +9 -16
  134. package/test/fixtures/joint-column.json +1138 -0
  135. package/test/fixtures/joint-roller.json +872 -0
  136. package/test/fixtures/robot-arm-6dof.json +2776 -0
  137. package/test/i18n-prefix-guard.test.ts +0 -31
  138. package/test/modeller-route.test.ts +2 -3
  139. package/test/v3-align.test.ts +9 -4
  140. package/test/v3-clips.test.ts +1 -1
  141. package/test/v3-controls.test.ts +1 -1
  142. package/test/v3-fields.test.ts +4 -4
  143. package/test/v3-first-save.test.ts +1 -1
  144. package/test/v3-gizmo.test.ts +5 -5
  145. package/test/v3-joint-fan.test.ts +12 -42
  146. package/test/v3-keys.test.ts +1 -1
  147. package/test/v3-layout.test.ts +4 -5
  148. package/test/v3-links.test.ts +3 -3
  149. package/test/v3-material.test.ts +3 -3
  150. package/test/v3-maturity.test.ts +8 -9
  151. package/test/v3-parts-wiring.test.ts +3 -3
  152. package/test/v3-preview.test.ts +1 -1
  153. package/test/v3-product-write.test.ts +1 -1
  154. package/test/v3-properties.test.ts +3 -3
  155. package/test/v3-refusals.test.ts +12 -0
  156. package/test/v3-release-panel.test.ts +5 -5
  157. package/test/v3-scene-canvas.test.ts +2 -2
  158. package/test/v3-theme.test.ts +2 -2
  159. package/test/v3-verdict.test.ts +2 -2
  160. package/test/v3-view.test.ts +2 -2
  161. package/translations/en.json +2 -143
  162. package/translations/ja.json +2 -143
  163. package/translations/ko.json +2 -143
  164. package/translations/ms.json +2 -143
  165. package/translations/zh.json +2 -143
  166. package/client/modeller/figure-source.ts +0 -437
  167. package/client/modeller/part-edits.ts +0 -312
  168. package/dist-client/modeller/figure-source.d.ts +0 -238
  169. package/dist-client/modeller/figure-source.js +0 -193
  170. package/dist-client/modeller/figure-source.js.map +0 -1
  171. package/dist-client/modeller/part-edits.d.ts +0 -89
  172. package/dist-client/modeller/part-edits.js +0 -270
  173. package/dist-client/modeller/part-edits.js.map +0 -1
  174. package/test/expect.ts +0 -57
  175. package/test/figure-source.test.ts +0 -511
  176. package/test/part-edits.test.ts +0 -350
@@ -1,312 +0,0 @@
1
- import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'
2
- import type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'
3
-
4
- import { AXIS_KEYS, draftExtent } from './figure-source.js'
5
- import type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'
6
-
7
- /**
8
- * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.
9
- *
10
- * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.
11
- * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.
12
- * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.
13
- *
14
- * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.
15
- * 여기서 다시 정의하지 않는다.
16
- *
17
- * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을
18
- * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.
19
- */
20
-
21
- /** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */
22
- function freeName(taken: Set<string>, base: string): string {
23
- if (!taken.has(base)) return base
24
- for (let n = 2; ; n++) {
25
- const candidate = `${base}-${n}`
26
- if (!taken.has(candidate)) return candidate
27
- }
28
- }
29
-
30
- const round = (value: number) => Math.round(value)
31
-
32
- /** 값을 두 끝 사이로 당긴다. 끝이 뒤집혀 있으면(부품이 상자보다 크다) 앞의 끝을 따른다. */
33
- function clamp(value: number, least: number, most: number): number {
34
- return Math.max(least, Math.min(most, value))
35
- }
36
-
37
- /** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
38
- export function partLimit(draft: FigureDraft): number {
39
- return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]
40
- }
41
-
42
- /**
43
- * 새 부품 하나를 만든다.
44
- *
45
- * 기준 상자의 3분의 1 크기다. 추가하자마자 화면에 보여야 한다 — 크기 0 으로 놓으면
46
- * 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
47
- *
48
- * ## 어디에 놓나 — 고른 부품 **위**다
49
- *
50
- * 전에는 언제나 바닥 한가운데였다. 첫 부품은 그 자리가 맞지만 둘째부터는 **첫 부품 속에**
51
- * 들어간다 — 같은 자리에 같은 크기로 놓이니 화면에는 덩어리 하나뿐이고, 저작자는 무엇이
52
- * 늘었는지 못 본다. 「추가하자마자 보여야 한다」는 그때 이미 깨져 있었다.
53
- *
54
- * 그래서 고른 부품이 있으면 그 부품의 **윗면**에 얹고 발자국을 가운데로 맞춘다. 기계는
55
- * 대개 그렇게 쌓인다(받침 → 어깨 → 팔). 관절을 걸 때도 이 자리가 필요하다 — 맞닿는 면이
56
- * 실제로 있어야 `seamOrigin` 이 회전 중심을 그 면에서 잡는다.
57
- *
58
- * 위에 얹으면 기준 상자를 넘는 경우에는 바닥 한가운데로 간다. 넘은 채로 놓으면 발행이
59
- * 막히고(`part-outside-base`), 그것을 고치는 일이 「부품을 추가했다」의 대가가 된다.
60
- * 가로·세로는 상자 안으로 당긴다 — 고른 부품이 가장자리에 있으면 그 위가 상자 밖이다.
61
- */
62
- export function newPart(
63
- draft: FigureDraft,
64
- parts: PartModel[],
65
- primitive: PrimitiveKind,
66
- onTopOf?: PartModel
67
- ): PartModel {
68
- const taken = new Set(parts.map(part => part.name))
69
-
70
- const width = Math.max(1, Math.round(draft.width / 3))
71
- const height = Math.max(1, Math.round(draft.height / 3))
72
- const depth = Math.max(1, Math.round(draft.depth / 3))
73
-
74
- // 바닥 한가운데 — 고를 부품이 없을 때, 그리고 위가 상자 밖일 때의 자리.
75
- const middle = {
76
- left: Math.round((draft.width - width) / 2),
77
- top: Math.round((draft.height - height) / 2),
78
- // `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.
79
- zPos: 0
80
- }
81
- const above = onTopOf && {
82
- left: clamp(Math.round(onTopOf.left + onTopOf.width / 2 - width / 2), 0, draft.width - width),
83
- top: clamp(Math.round(onTopOf.top + onTopOf.height / 2 - height / 2), 0, draft.height - height),
84
- zPos: Math.round(onTopOf.zPos + onTopOf.depth)
85
- }
86
- const at = above && above.zPos + depth <= draft.depth ? above : middle
87
-
88
- const part: PartModel = {
89
- type: 'figure-part',
90
- name: freeName(taken, primitive),
91
- primitive,
92
- left: at.left,
93
- top: at.top,
94
- width,
95
- height,
96
- zPos: at.zPos,
97
- depth,
98
- // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.
99
- token: parts[0]?.token ?? 'palette.primary'
100
- }
101
-
102
- if (SEGMENTED_PRIMITIVES.includes(primitive)) {
103
- part.segments = SEGMENT_PRESETS[1]
104
- }
105
- if (EXTRUDE_PRIMITIVES.includes(primitive)) {
106
- part.round = 0
107
- }
108
- if (primitive === 'polygon') {
109
- // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.
110
- // 단면의 점은 부품 중심을 기준으로 적는다.
111
- part.path = [
112
- { x: -Math.round(width / 2), y: -Math.round(height / 2) },
113
- { x: Math.round(width / 2), y: -Math.round(height / 2) },
114
- { x: 0, y: Math.round(height / 2) }
115
- ]
116
- }
117
-
118
- return part
119
- }
120
-
121
- /**
122
- * 씬에서 온 부품을 밀리미터로 맞춘다.
123
- *
124
- * 기즈모는 비율을 곱해서 크기를 낸다 — 420 에 1.72 를 곱하면 722.4180046035418 이고, 손대지
125
- * 않은 축도 384.99999999999994 처럼 흔들린다. 저작자가 끈 결과가 소수 넷째 자리까지 남을
126
- * 까닭이 없고, 칸에는 「722.4…」로 잘려 보인다. 이 편집기의 다른 길은 전부 정수 밀리미터로
127
- * 맞춘다(부품을 새로 놓을 때 · 가운데 맞추기 · 격자 맞추기).
128
- *
129
- * 크기는 1 밑으로 내려가지 않는다 — 0 이면 부피가 사라지고 배율이 0 으로 나뉜다.
130
- */
131
- export function roundPart(part: PartModel): PartModel {
132
- return {
133
- ...part,
134
- left: round(part.left),
135
- top: round(part.top),
136
- zPos: round(part.zPos),
137
- width: Math.max(1, round(part.width)),
138
- height: Math.max(1, round(part.height)),
139
- depth: Math.max(1, round(part.depth))
140
- }
141
- }
142
-
143
- export function addPart(
144
- draft: FigureDraft,
145
- parts: PartModel[],
146
- primitive: PrimitiveKind,
147
- onTopOf?: PartModel
148
- ): PartModel[] {
149
- return [...parts, newPart(draft, parts, primitive, onTopOf)]
150
- }
151
-
152
- export function removePart(parts: PartModel[], index: number): PartModel[] {
153
- return parts.filter((_, at) => at !== index)
154
- }
155
-
156
- /** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */
157
- export function duplicatePart(parts: PartModel[], index: number): PartModel[] {
158
- const origin = parts[index]
159
- if (!origin) return parts
160
-
161
- const copy: PartModel = JSON.parse(JSON.stringify(origin))
162
- copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)
163
-
164
- const next = [...parts]
165
- next.splice(index + 1, 0, copy)
166
- return next
167
- }
168
-
169
- /** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */
170
- export function movePart(parts: PartModel[], index: number, to: number): PartModel[] {
171
- if (to < 0 || to >= parts.length || index === to) return parts
172
-
173
- const next = [...parts]
174
- const [moved] = next.splice(index, 1)
175
- next.splice(to, 0, moved!)
176
- return next
177
- }
178
-
179
- export function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {
180
- return parts.map((old, at) => (at === index ? part : old))
181
- }
182
-
183
- /**
184
- * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.
185
- *
186
- * 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을
187
- * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로
188
- * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.
189
- */
190
-
191
- function put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {
192
- const part = parts[index]
193
- if (!part) return parts
194
- return updatePart(parts, index, { ...part, ...fields })
195
- }
196
-
197
- /** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */
198
- function putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {
199
- return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)
200
- }
201
-
202
- /** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */
203
- export function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
204
- const part = parts[index]
205
- if (!part) return parts
206
-
207
- return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))
208
- }
209
-
210
- /** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */
211
- export function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
212
- return putAt(parts, index, axis, 0)
213
- }
214
-
215
- /** 부품을 상자의 끝에 붙인다. */
216
- export function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
217
- const part = parts[index]
218
- if (!part) return parts
219
-
220
- return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))
221
- }
222
-
223
- /**
224
- * 부품을 축 기준으로 대칭 복제한다.
225
- *
226
- * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수
227
- * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.
228
- */
229
- export function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
230
- const part = parts[index]
231
- if (!part || !canMirror(draft, parts, index, axis)) return parts
232
-
233
- const copy = JSON.parse(JSON.stringify(part)) as PartModel
234
- copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)
235
-
236
- const next = [...parts]
237
- next.splice(index + 1, 0, copy)
238
- return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))
239
- }
240
-
241
- /** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */
242
- function mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {
243
- const { at, size } = AXIS_KEYS[axis]
244
- return Math.round(draftExtent(draft, axis) - part[at] - part[size])
245
- }
246
-
247
- /**
248
- * 대칭이 의미가 있는지.
249
- *
250
- * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는
251
- * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.
252
- * 만들어 놓고 알리는 대신 미리 막는다.
253
- */
254
- export function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {
255
- const part = parts[index]
256
- if (!part) return false
257
-
258
- return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])
259
- }
260
-
261
- /**
262
- * 부품의 위치와 크기를 격자에 맞춘다.
263
- *
264
- * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
265
- * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.
266
- */
267
- export function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {
268
- const part = parts[index]
269
- if (!part) return parts
270
-
271
- const step = gridStep(draft)
272
- const round = (value: number) => Math.round(value / step) * step
273
-
274
- return put(parts, index, {
275
- left: round(part.left),
276
- top: round(part.top),
277
- zPos: round(part.zPos),
278
- // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다
279
- width: Math.max(step, round(part.width)),
280
- height: Math.max(step, round(part.height)),
281
- depth: Math.max(step, round(part.depth))
282
- })
283
- }
284
-
285
- /**
286
- * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.
287
- *
288
- * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워
289
- * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가
290
- * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.
291
- */
292
- export function gridStep(draft: FigureDraft): number {
293
- return niceStep(Math.max(draft.width, draft.height, draft.depth))
294
- }
295
-
296
- /**
297
- * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.
298
- *
299
- * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기
300
- * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.
301
- *
302
- * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이
303
- * 먼저 보였다.
304
- */
305
- export function niceStep(span: number): number {
306
- const raw = Math.max(span, 1) / 5
307
- const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))
308
- const n = raw / magnitude
309
-
310
- const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10
311
- return Math.max(1, multiple * magnitude)
312
- }
@@ -1,238 +0,0 @@
1
- import { type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model';
2
- /**
3
- * Scene model <-> `FigureSource`.
4
- *
5
- * ## Why this is the only place that converts
6
- *
7
- * While authoring, the source of truth is the **scene model** -- part
8
- * components sitting on a draft. `FigureSource` is produced from it when the
9
- * asset is saved or scored.
10
- *
11
- * The two use different vocabularies. The scene inherited 2D canvas terms
12
- * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for
13
- * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`
14
- * front, position at the part's **centre**).
15
- *
16
- * **Convert in one place.** Two places drift, and this format has already had
17
- * one incident caused by axes disagreeing.
18
- *
19
- * ## It was called `fold`, and that was wrong
20
- *
21
- * `fold` is an established term -- it reduces a structure to a summary, and
22
- * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing
23
- * is reduced here: the same information comes out in a different shape, and it
24
- * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this
25
- * one as reducing something, and the user did exactly that.
26
- *
27
- * ## The mapping
28
- *
29
- * The draft is the base box; its centre is the format's origin.
30
- *
31
- * format scene
32
- * ──────────────── ──────────────────────────────
33
- * base.x draft width
34
- * base.y draft depth <- height
35
- * base.z draft height
36
- *
37
- * size.x width
38
- * size.y depth
39
- * size.z height
40
- *
41
- * position.x left + width/2 - draft width/2
42
- * position.y zPos + depth/2 <- both measured from the box bottom
43
- * position.z top + height/2 - draft height/2
44
- *
45
- * (Source edition 2 measures y from the bottom face of the base box, as the scene's zPos does, so
46
- * that row needs no half-box shift. x and z are still measured from the box centre. ADR-0065.)
47
- *
48
- * rotation.x rotationX (scene radians, format degrees)
49
- * rotation.y -rotation
50
- * rotation.z rotationY
51
- *
52
- * The rotation names are crossed because of the scene: `rotationY` becomes
53
- * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.
54
- *
55
- * ## Fields nothing reads yet still travel
56
- *
57
- * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format
58
- * and the editor ignores them. They still have to survive a round trip. When a
59
- * source proposed by AI is opened, edited and written back, a field that
60
- * disappears **without an error** is one the author cannot know about.
61
- *
62
- * **Five have gone that way here**: `ports`, `animations`, `anchor`,
63
- * `shape.hollow` and `keepRound`. The last one the test caught first, which
64
- * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the
65
- * floor no matter what the sample said; the fourth as a hollowed tray that
66
- * came out a solid block.
67
- *
68
- * All four passed the round-trip test, because the fixture it uses did not
69
- * carry the field. So the test now reads the **field list the format declares**
70
- * rather than trusting the fixture.
71
- *
72
- * `shape` is unpacked into `round`, `path` and `hollow` rather than carried
73
- * whole, because the editor holds those as separate state. Every field added
74
- * under `shape` costs a line here, and forgetting that line was the fourth
75
- * of the four.
76
- */
77
- /** 판 컴포넌트의 상태. 씬의 용어 그대로다. */
78
- export interface FigureDraft {
79
- /** 형식 버전. */
80
- version?: number;
81
- /** 가로 너비 (mm) → `base.x`. */
82
- width: number;
83
- /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */
84
- height: number;
85
- /** 높이 (mm) → `base.y`. 서 있는 방향이다. */
86
- depth: number;
87
- /** 컴포넌트 타입 이름. **저장되는 식별자다.** */
88
- figureType?: string;
89
- /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */
90
- placement?: FigurePlacement;
91
- detailLevel?: DetailLevel;
92
- styleKit?: string;
93
- /**
94
- * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.
95
- *
96
- * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다
97
- * 저장하는 것만으로 **조용히 지워지기** 때문이다.
98
- */
99
- animations?: FigureSource['animations'];
100
- /**
101
- * 값이 만드는 자세 — **애니메이션과 다른 칸이다.**
102
- *
103
- * 저것은 시각이 흘러 움직이는 것(도는 롤러)이고 이것은 인스턴스가 준 값이 그대로 자세가
104
- * 되는 것(1200mm 내려온 호이스트)이다. 한동안 애니메이션 안에 `drive: 'hold'` 로 숨어
105
- * 있었고, 그래서 「길이값을 변수로 노출하라」는 요청에 아무도 이것을 못 찾았다(ADR-0051).
106
- */
107
- parameters?: FigureSource['parameters'];
108
- /**
109
- * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.
110
- *
111
- * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가
112
- * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐
113
- * 주나**를 말하는 다른 칸이다.
114
- */
115
- capabilities?: FigureSource['capabilities'];
116
- /**
117
- * How parts move relative to their parents (ADR-0066). Kept in figure coordinates as stored, the same
118
- * terms the inspector shows. Carried even when nothing edits them, so opening and saving never drops
119
- * them.
120
- */
121
- joints?: FigureSource['joints'];
122
- }
123
- /** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */
124
- export interface PartModel {
125
- type: 'figure-part';
126
- /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */
127
- name: string;
128
- primitive: FigurePart['primitive'];
129
- left: number;
130
- top: number;
131
- width: number;
132
- height: number;
133
- /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */
134
- zPos: number;
135
- depth: number;
136
- /** 라디안. 씬이 그렇게 쓴다. */
137
- rotationX?: number;
138
- rotation?: number;
139
- rotationY?: number;
140
- segments?: number;
141
- round?: number;
142
- path?: {
143
- x: number;
144
- y: number;
145
- }[];
146
- /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */
147
- hollow?: NonNullable<FigurePart['shape']>['hollow'];
148
- /**
149
- * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**
150
- *
151
- * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로
152
- * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에
153
- * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.
154
- *
155
- * 숨김은 "지금 이 뒤를 만지려고" 하는 것이지 그 부품의 성질이 아니다. 그래서
156
- * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.
157
- */
158
- hidden?: boolean;
159
- /**
160
- * 씬이 이것을 **집지 않는다** — 클릭·마퀴·기즈모 어디에도 안 걸린다.
161
- *
162
- * 저작면에만 서는 것(관절 부채꼴)이 쓴다. 저장 형식에는 없는 칸이라 `toFigureSource` 가
163
- * 싣지 않는다.
164
- */
165
- locked?: boolean;
166
- /** 색 토큰 이름 (`palette.primary` 등). */
167
- token?: string;
168
- preset?: FigurePart['material']['preset'];
169
- flatShading?: boolean;
170
- transparent?: boolean;
171
- /**
172
- * 자체발광. 없으면 안 빛난다.
173
- *
174
- * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진
175
- * 전구는 빨갛지 않고 어두운 플라스틱이다.
176
- */
177
- emissive?: FigurePart['material']['emissive'];
178
- surface?: FigurePart['material']['surface'];
179
- materialSlot?: FigurePart['materialSlot'];
180
- /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */
181
- keepRound?: FigurePart['keepRound'];
182
- sizing?: FigurePart['sizing'];
183
- /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */
184
- anchor?: FigurePart['anchor'];
185
- repeat?: FigurePart['repeat'];
186
- label?: FigurePart['label'];
187
- /**
188
- * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.
189
- *
190
- * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,
191
- * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.
192
- */
193
- capability?: FigurePart['capability'];
194
- /** The part this one is attached to (ADR-0066). The one place a parent is written. */
195
- parent?: FigurePart['parent'];
196
- }
197
- /** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */
198
- export declare function round4(value: number): number;
199
- /**
200
- * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.
201
- *
202
- * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.
203
- */
204
- export declare function halfOf(draft: FigureDraft): Vec3;
205
- /**
206
- * 저작자가 보는 축이 씬의 어느 용어인가.
207
- *
208
- * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —
209
- * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.
210
- */
211
- export declare const AXIS_KEYS: {
212
- readonly x: {
213
- readonly at: "left";
214
- readonly size: "width";
215
- };
216
- readonly y: {
217
- readonly at: "zPos";
218
- readonly size: "depth";
219
- };
220
- readonly z: {
221
- readonly at: "top";
222
- readonly size: "height";
223
- };
224
- };
225
- export type PlaneAxis = keyof typeof AXIS_KEYS;
226
- /** 판이 그 축으로 얼마나 넓은가. */
227
- export declare function draftExtent(draft: FigureDraft, axis: PlaneAxis): number;
228
- /** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */
229
- export declare function partToFigure(part: PartModel, draft: FigureDraft): FigurePart;
230
- /** 부품 하나를 씬 용어로 되돌린다. */
231
- export declare function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel;
232
- /** 씬 모델을 저장 형식으로 변환한다. */
233
- export declare function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource;
234
- /** 형식을 씬 모델로 편다. */
235
- export declare function fromFigureSource(source: FigureSource): {
236
- draft: FigureDraft;
237
- parts: PartModel[];
238
- };