@hatiolab/figure-model 0.1.76 → 0.1.78

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 (135) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +60 -138
  3. package/dist/index.d.ts +1 -13
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +3 -26
  6. package/dist/index.js.map +1 -1
  7. package/dist/v3-asset-types.d.ts +1 -2
  8. package/dist/v3-asset-types.d.ts.map +1 -1
  9. package/dist/v3-asset.js +1 -1
  10. package/dist/v3-asset.js.map +1 -1
  11. package/dist/v3-authoring-actions.d.ts +9 -12
  12. package/dist/v3-authoring-actions.d.ts.map +1 -1
  13. package/dist/v3-authoring-actions.js +4 -5
  14. package/dist/v3-authoring-actions.js.map +1 -1
  15. package/dist/v3-capabilities.js +4 -1
  16. package/dist/v3-capabilities.js.map +1 -1
  17. package/dist/v3-cost.d.ts.map +1 -1
  18. package/dist/v3-cost.js +3 -5
  19. package/dist/v3-cost.js.map +1 -1
  20. package/dist/v3-create.d.ts +1 -1
  21. package/dist/v3-create.d.ts.map +1 -1
  22. package/dist/v3-examples.js +1 -1
  23. package/dist/v3-gate.d.ts +7 -8
  24. package/dist/v3-gate.d.ts.map +1 -1
  25. package/dist/v3-gate.js +11 -14
  26. package/dist/v3-gate.js.map +1 -1
  27. package/dist/v3-graph.d.ts +1 -2
  28. package/dist/v3-graph.d.ts.map +1 -1
  29. package/dist/v3-graph.js +5 -7
  30. package/dist/v3-graph.js.map +1 -1
  31. package/dist/v3-instance-size.d.ts +3 -5
  32. package/dist/v3-instance-size.d.ts.map +1 -1
  33. package/dist/v3-instance-size.js +2 -4
  34. package/dist/v3-instance-size.js.map +1 -1
  35. package/dist/v3-kernel-version.d.ts +1 -1
  36. package/dist/v3-kernel-version.js +1 -1
  37. package/dist/v3-part-edit.d.ts +4 -5
  38. package/dist/v3-part-edit.d.ts.map +1 -1
  39. package/dist/v3-part-edit.js +2 -2
  40. package/dist/v3-part-edit.js.map +1 -1
  41. package/dist/v3-proposal.js +1 -1
  42. package/dist/v3-proposal.js.map +1 -1
  43. package/dist/v3-release-contract.js +1 -1
  44. package/dist/v3-release-contract.js.map +1 -1
  45. package/dist/v3-score.d.ts +4 -5
  46. package/dist/v3-score.d.ts.map +1 -1
  47. package/dist/v3-score.js +7 -8
  48. package/dist/v3-score.js.map +1 -1
  49. package/dist/v3-words.d.ts +105 -0
  50. package/dist/v3-words.d.ts.map +1 -0
  51. package/dist/v3-words.js +107 -0
  52. package/dist/v3-words.js.map +1 -0
  53. package/docs/assembly-constraints-v3.md +13 -23
  54. package/docs/v3-asset-persistence.md +4 -4
  55. package/docs/v3-common-kernel-experiment.md +1 -1
  56. package/docs/v3-core-semantics.md +3 -3
  57. package/docs/v3-cutover.md +5 -3
  58. package/docs/v3-design.md +13 -26
  59. package/docs/v3-editor-integration.md +2 -2
  60. package/docs/v3-full-conveyor-validation.md +3 -3
  61. package/docs/v3-layout-system.md +1 -1
  62. package/docs/v3-minimal-model.md +0 -1
  63. package/docs/v3-motion-contract.md +21 -56
  64. package/docs/v3-runtime-extraction.md +7 -7
  65. package/docs/v3-sample-validation-expanded.md +1 -1
  66. package/docs/v3-sample-validation.md +1 -1
  67. package/docs/v3-shape-dimension-contract.md +23 -57
  68. package/docs/v3-status.md +5 -5
  69. package/docs/v3-storage-integration.md +4 -4
  70. package/package.json +2 -20
  71. package/dist/blueprint-shape.d.ts +0 -10
  72. package/dist/blueprint-shape.d.ts.map +0 -1
  73. package/dist/blueprint-shape.js +0 -194
  74. package/dist/blueprint-shape.js.map +0 -1
  75. package/dist/blueprint.d.ts +0 -73
  76. package/dist/blueprint.d.ts.map +0 -1
  77. package/dist/blueprint.js +0 -388
  78. package/dist/blueprint.js.map +0 -1
  79. package/dist/cost-chart.d.ts +0 -35
  80. package/dist/cost-chart.d.ts.map +0 -1
  81. package/dist/cost-chart.js +0 -150
  82. package/dist/cost-chart.js.map +0 -1
  83. package/dist/cost.d.ts +0 -162
  84. package/dist/cost.d.ts.map +0 -1
  85. package/dist/cost.js +0 -194
  86. package/dist/cost.js.map +0 -1
  87. package/dist/gate.d.ts +0 -65
  88. package/dist/gate.d.ts.map +0 -1
  89. package/dist/gate.js +0 -284
  90. package/dist/gate.js.map +0 -1
  91. package/dist/grouping.d.ts +0 -121
  92. package/dist/grouping.d.ts.map +0 -1
  93. package/dist/grouping.js +0 -224
  94. package/dist/grouping.js.map +0 -1
  95. package/dist/keys.d.ts +0 -45
  96. package/dist/keys.d.ts.map +0 -1
  97. package/dist/keys.js +0 -84
  98. package/dist/keys.js.map +0 -1
  99. package/dist/origin.d.ts +0 -33
  100. package/dist/origin.d.ts.map +0 -1
  101. package/dist/origin.js +0 -46
  102. package/dist/origin.js.map +0 -1
  103. package/dist/release-contract.d.ts +0 -38
  104. package/dist/release-contract.d.ts.map +0 -1
  105. package/dist/release-contract.js +0 -246
  106. package/dist/release-contract.js.map +0 -1
  107. package/dist/sizing.d.ts +0 -231
  108. package/dist/sizing.d.ts.map +0 -1
  109. package/dist/sizing.js +0 -550
  110. package/dist/sizing.js.map +0 -1
  111. package/dist/types.d.ts +0 -1233
  112. package/dist/types.d.ts.map +0 -1
  113. package/dist/types.js +0 -547
  114. package/dist/types.js.map +0 -1
  115. package/dist/v3-from-v2.d.ts +0 -203
  116. package/dist/v3-from-v2.d.ts.map +0 -1
  117. package/dist/v3-from-v2.js +0 -2053
  118. package/dist/v3-from-v2.js.map +0 -1
  119. package/dist/v3-mesh-compare.d.ts +0 -24
  120. package/dist/v3-mesh-compare.d.ts.map +0 -1
  121. package/dist/v3-mesh-compare.js +0 -55
  122. package/dist/v3-mesh-compare.js.map +0 -1
  123. package/dist/validate.d.ts +0 -7
  124. package/dist/validate.d.ts.map +0 -1
  125. package/dist/validate.js +0 -1119
  126. package/dist/validate.js.map +0 -1
  127. package/dist/visual-evidence.d.ts +0 -21
  128. package/dist/visual-evidence.d.ts.map +0 -1
  129. package/dist/visual-evidence.js +0 -52
  130. package/dist/visual-evidence.js.map +0 -1
  131. package/docs/design.md +0 -193
  132. package/docs/format-survey.md +0 -108
  133. package/docs/format.md +0 -704
  134. package/docs/sizing.md +0 -183
  135. package/docs/validation.md +0 -357
package/docs/format.md DELETED
@@ -1,704 +0,0 @@
1
- # 형식 레퍼런스
2
-
3
- 원본 데이터(`FigureSource`)와 청사진(`FigureBlueprint`)의 모든 필드.
4
-
5
- ```
6
- FigureSource 저작 결과. 편집 가능한 정본. DB 에 저장된다.
7
- FigureBlueprint 씬이 세우는 형태. 정본에서 파생되는 캐시. 저장하지 않는다.
8
- ```
9
-
10
- 값의 기준이 되는 원본은 `src/types.ts` 다. 이 문서와 코드가 어긋나면 `src/docs.test.ts` 가 실패한다.
11
-
12
- ## 좌표 규약
13
-
14
- 3D 도구들이 쓰는 그대로다 — glTF · three.js · Unity · Godot 와 같다.
15
-
16
- ```
17
- x 오른쪽
18
- y 위
19
- z 앞 (보는 쪽)
20
- ```
21
-
22
- 크기도 축 이름으로 부른다. `width` · `height` · `depth` 는 쓰지 않는다 — 어느 축인지
23
- 따로 외워야 하는 용어이고, 실제로 그것 때문에 사고가 났다.
24
-
25
- ```
26
- transform.position { x, y, z } 부품 중심의 자리 [mm]
27
- transform.size { x, y, z } 축별 크기 [mm]
28
- transform.rotation { x, y, z } 축별 회전(도, deg), 선택. 오일러 각 적용 순서는 Three.js/glTF 기본인 'XYZ' (Rx → Ry → Rz)다.
29
- ```
30
-
31
- **원점은 부품의 중심**이고 `position` 은 그 원점의 자리다 — 여느 모델러와 같다.
32
-
33
- 그 자리를 재는 기준은 **`base` 상자의 밑면 가운데**다(원본 판 2, ADR-0065).
34
-
35
- ```
36
- x 상자 중심에서
37
- z 상자 중심에서
38
- y 상자 밑면에서 위로
39
- ```
40
-
41
- 바닥에 선 부품은 `position.y` 가 자기 높이의 절반이다. 상자 높이(`base.y`)를 바꿔도 부품의 수는
42
- 그대로다 — 편집하면서 높이가 계속 달라지기 때문에 이 기준을 쓴다. 놓이는 자산에 흔한 원점(발 밑
43
- 가운데)과도 같다.
44
-
45
- 컴파일한 청사진은 y 도 상자 중심에서 잰다. 씬 런타임의 원점 규약이 그것이고, 원본을 그 좌표로 바꾸는
46
- 자리는 `origin.ts` 한 곳이다.
47
-
48
- 판 1(2026-09-17 이전)은 y 를 상자 중심에서 쟀다. 판을 적지 않은 원본도 판 1 이다. **판 2 만 읽는다** —
49
- 다른 판은 `source-version-unsupported` 로 거절한다.
50
-
51
- 씬 컴포넌트의 state 는 `width` · `height` · `depth` 이고 축 배정도 다르다(그쪽은
52
- `depth` 가 높이다). 그 환산은 그리는 쪽 경계 한 곳에서 한다.
53
-
54
- ## 단위 규약
55
-
56
- 모든 기하 치수(`base`, `position`, `size`, `pitch`)의 기본 단위는 **밀리미터(mm)**다.
57
-
58
- ```
59
- base: { x: 1200, y: 800, z: 1000 } // 1200mm × 800mm × 1000mm (1.2m × 0.8m × 1.0m)
60
- pitch: 150 // 롤러 중심 간 거리 150mm
61
- ```
62
-
63
- 디지털 트윈 및 산업 공장 씬에서 150은 150mm(0.15m)를 의미하며, 미터(m) 등 다른 단위와의 혼용을 금지한다.
64
-
65
- ---
66
-
67
- ## `FigureSource` — 원본 데이터
68
-
69
- | 필드 | 타입 | 필수 | 뜻 |
70
- | ------------- | -------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------- |
71
- | `version` | `number` | | 형식 판 번호. 기본값 1. 스키마 버전 명시 및 마이그레이션 기준선 |
72
- | `type` | `string` | ✓ | 컴포넌트 타입 이름. **저장되는 식별자다** — 씬이 이 이름으로 청사진을 찾는다. 바꾸면 이미 놓인 인스턴스가 세워지지 않는다 |
73
- | `base` | `Vec3` | ✓ | 기준 크기(축별, mm). 배치 크기가 이 비율에서 나온다 |
74
- | `parts` | `FigurePart[]` | ✓ | 부품. 하나는 있어야 한다 |
75
- | `detailLevel` | `'S' \| 'M' \| 'L'` | | 부품 수 상한을 정한다 (3 · 8 · 20) |
76
- | `styleKit` | `string` | | StyleKit 이름. 팔레트·분할 수·그리드·등급을 한 번에 받는다 |
77
- | `placement` | `'floor' \| 'ceiling' \| 'center'` | | 어느 면에 붙나 — 아래 |
78
- | `animations` | `AnimationClip[]` | | 시각이 흘러서 움직이는 것 — 아래 |
79
- | `parameters` | `FigureParameter[]` | | 값이 바뀌어서 움직이는 것 — 아래 |
80
- | `capabilities`| `FigureCapability[]` | | 흐름에서 무엇을 하나 — 아래 |
81
-
82
- ### `placement` — 어느 면에 붙나
83
-
84
- 컨베이어는 바닥에 서고 OHT 는 천장에 매달린다. 그것은 **그 종류의 사실**이지 놓는 사람이
85
- 매번 정할 것이 아니다. 그래서 정본이 지고 다니고, 인스턴스가 배치될 때 씬이 높이를 정한다.
86
-
87
- ```
88
- floor 바닥에 선다. 안 적으면 이것이다 컨베이어 · 랙 · 작업대
89
- ceiling 천장에 매달린다 OHT · 호이스트 · 크레인
90
- center 공중에 뜬다. 기준 면이 없다 드론 · 센서
91
- ```
92
-
93
- **저작하는 좌표는 이것과 무관하다.** 어느 값이든 부품은 `base` 상자 안의 같은 자리에 적힌다
94
- — `position.y` 는 언제나 상자 **중심**에서 잰다. 천장 기준 도형이라고 해서 숫자가 뒤집히지
95
- 않는다. 뒤집으면 저작자가 적는 수가 드롭다운 하나에 따라 다른 면을 뜻하게 된다.
96
-
97
- ## `FigurePart` — 부품
98
-
99
- | 필드 | 타입 | 필수 | 뜻 |
100
- | ----------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- |
101
- | `name` | `string` | ✓ | **저장되는 식별자다.** 바인딩·애니메이션·슬롯이 이 이름을 가리킨다. 바꾸면 인스턴스의 바인딩이 오류보고없이 끊긴다. 한 Figure 안에서 겹칠 수 없다 |
102
- | `primitive` | `PrimitiveKind` | ✓ | 아래 표 |
103
- | `transform` | `PartTransform` | ✓ | 자리와 크기 |
104
- | `material` | `PartMaterial` | ✓ | 색과 질감 |
105
- | `segments` | `number` | | 곡면 분할 수. `cylinder` · `sphere` 만 |
106
- | `shape` | `PartShape` | | 단면. `rect` · `polygon` 만. `hollow` 로 **속을 판다** |
107
- | `materialSlot` | `'cap' \| 'side'` | | 상태 색이 연동될 재질 슬롯. 없으면 상태를 표현하지 않는다. **`slot` 이 아니다** — 그 낱말은 보유물이 앉는 자리 쪽 임자가 있다 |
108
- | `sizing` | `SizingRule` | | 크기 반응 규칙. 기본 `'scale'` |
109
- | `keepRound` | `boolean` | | 단면을 원으로 지킬까. 곡면만. 기본 `true` — **켜면 draw call 하나를 쓴다** |
110
- | `anchor` | `PartAnchor` | | 축마다 **어느 면을 붙잡나**. `sizing` 이 정한 것을 덮어쓴다 |
111
- | `repeat` | `{ axis, pitch }` | | `sizing` 이 `'repeat'` 일 때 필수 (`pitch` 단위: mm) |
112
- | `capability`| `PartCapability` | | 부품이 갖는 닻(능력). 그리지 않고 자리(`slot`) 또는 문(`port`)으로 컴파일된다 |
113
- | `label` | `LabelSpec` | | 텍스트. 아래 표 |
114
- | `parent` | `string` | | 붙어 있는 부품의 이름. 없으면 figure 의 틀에 붙는다. **부모를 적는 자리는 여기 하나다** — 아래 「관절」 |
115
-
116
- ### `PrimitiveKind`
117
-
118
- | 값 | 계열 | 분할 수 | 단면 |
119
- | ---------- | ------- | ------- | ------------------------ |
120
- | `cube` | mesh | | |
121
- | `wall` | mesh | | |
122
- | `cylinder` | mesh | ✓ | |
123
- | `sphere` | mesh | ✓ | |
124
- | `rect` | extrude | | `shape.round` |
125
- | `polygon` | extrude | | `shape.path` (3 점 이상) |
126
-
127
- mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민다.
128
-
129
- ### `PartTransform`
130
-
131
- | 필드 | 타입 | 필수 | 뜻 |
132
- | ---------- | --------------- | ---- | ----------------- |
133
- | `position` | `Vec3` | ✓ | 부품 중심의 자리 |
134
- | `size` | `Vec3` | ✓ | 축별 크기 |
135
- | `rotation` | `Partial<Vec3>` | | 축별 회전(도) |
136
-
137
- `size.y` 를 빼면 오류다. **0 으로 갈음하지 않는다** — 두께를 모르는 것과 두께가 0 인
138
- 것은 다르다. `size.y` 가 0 이면 바닥에 깔린 평면 단면이다.
139
-
140
- ### `PartMaterial`
141
-
142
- | 필드 | 타입 | 뜻 |
143
- | ------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
144
- | `token` | `string` | 팔레트 토큰. 예: `'palette.primary'`. **값이 아니라 이름을 둔다** — 값을 구우면 테마를 바꿔도 안 따라오고, 저작한 자산 수백 개를 하나씩 고쳐야 한다 |
145
- | `preset` | `MaterialPreset` | `default` · `metal` · `glass` · `plastic` · `wood` · `ceramic` · `rubber` |
146
- | `flatShading` | `boolean` | 면마다 법선 하나. **로우폴리의 핵심 스위치다** — 분할이 낮은 원기둥은 smooth 에서 「덜 그린 원기둥」이고 flat 에서 「의도한 각기둥」이다 |
147
- | `transparent` | `boolean` | 별도 정렬 경로를 탄다 |
148
- | `surface` | `PartSurface` | **예외.** 아래 표 |
149
-
150
- `token` 과 `preset` 중 하나는 있어야 한다.
151
-
152
- #### 색은 팔레트 토큰 하나뿐이다 — 빠뜨린 것이 아니라 금지한 것이다
153
-
154
- 무늬·그라디언트를 담을 항목이 `PartMaterial` 에 없는 것은 아직 만들지 않아서가 아니다.
155
- 두 이유에서 일부러 뺐다.
156
-
157
- | | 이유 |
158
- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
159
- | 미감 | 색을 자유롭게 열면 로우폴리가 무너진다. 팔레트 토큰 몇 개로 제한하는 것이 「감각적인 로우폴리」의 첫 규칙이다 |
160
- | 성능 | 단색이어야 **재질을 나눠 쓴다.** 무늬·그라디언트는 인스턴스마다 캔버스 텍스처가 필요해 재질이 인스턴스 수만큼 생긴다 — 캐리어 1만 개면 재질 1만 개 |
161
-
162
- **두 이유가 같은 규칙을 가리킨다.**
163
-
164
- ### `PartSurface` — 예외 통로
165
-
166
- 정말 그것이어야 하는 자리(라벨 판, 표시창)에만 쓴다. **막지는 않지만 셈에 걸린다.**
167
-
168
- | 필드 | 타입 | 뜻 |
169
- | ------ | ------------------------- | ----------------------------------------------------------- |
170
- | `kind` | `'gradient' \| 'pattern'` | 둘 다 인스턴스마다 캔버스 텍스처가 필요하다 |
171
- | `ref` | `string` | 무엇을 쓰나. 텍스처 이름·이미지 키. 해석은 그리는 쪽이 한다 |
172
-
173
- 한 Figure 에 `LIMITS.surfaceTextures`(1) 개까지 봐준다. 넘으면 위반이고
174
- 원가표(`costOf`)의 재질 수와 점수(`scoreOf`)에 그대로 드러난다.
175
-
176
- ### 움직임 — 두 갈래다
177
-
178
- 도형이 움직이는 까닭은 둘 중 하나다.
179
-
180
- **시각이 흘러서 움직인다.** 컨베이어 롤러는 아무도 안 봐도 돌고 있다. 인스턴스가 주는
181
- 수는 **배속**이고, 1 이 보통이고 0 이면 선다. 이런 것이 `animations` 다.
182
-
183
- **값이 바뀌어서 움직인다.** 호이스트는 재생되는 것이 아니라 **지금 1200mm 내려와 있는**
184
- 것이다. 인스턴스가 주는 수는 **그 물리량**이고, 그 값이 곧 자세다. 이런 것이
185
- `parameters` 다.
186
-
187
- 한동안 뒤엣것이 앞엣것 안에 `drive: 'hold'` 로 들어 있었다. 형식의 판단은 맞았는데
188
- **자리와 이름이 틀려서**, 「호이스트 길이를 변수로 노출하라」는 요청에 저작자도 AI 도
189
- 이것을 못 찾았다 — 애니메이션 칸에 있을 거라고 생각하지 않는다. 지금은 둘이 갈라져 있고,
190
- `drive` 칸은 없어졌다. 업계도 같은 자리에서 가른다: Unity 의 Animation Clip 과 Blend Tree
191
- 파라미터, Blender 의 Action 과 Driver.
192
-
193
- ```
194
- FigureSource.animations[] 시각이 흐르는 것
195
- └ channels[] (부품, 경로) + 키프레임
196
-
197
- FigureSource.parameters[] 값이 자세를 만드는 것
198
- ├ range 인스턴스와 주고받는 물리량 — mm · deg · %
199
- └ clip.channels[] 같은 채널 형식이다
200
- ```
201
-
202
- **곡선은 둘 다 glTF 그대로다.** 이 형식을 다른 3D 형식으로 내보낼 수 있어야 하고, 그러려면
203
- 구조가 같아야 한다. 내보내면 평범한 glTF 애니메이션이 나가고, 못 받는 쪽은 **`range` 선언만
204
- 잃고 형상은 잃지 않는다.**
205
-
206
- #### `AnimationClip` — 시각이 흐르는 것
207
-
208
- | 필드 | 타입 | 뜻 |
209
- | ---------- | --------------------- | ------------------------------------------------------------ |
210
- | `name` | `string` | **저장되는 식별자.** 인스턴스가 이 이름으로 배속을 준다 |
211
- | `channels` | `AnimationChannel[]` | 하나는 있어야 한다 |
212
-
213
- 길이는 적지 않는다 — **마지막 키의 시각이 곧 길이다.** 따로 적으면 어긋날 수 있고,
214
- 어긋나면 어느 쪽이 맞는지 알 방법이 없다.
215
-
216
- #### `AnimationChannel`
217
-
218
- | 필드 | 타입 | 뜻 |
219
- | --------------- | ---------------------------------------- | ---------------------------------------------------------- |
220
- | `target` | `string` | 어느 부품인가. `FigurePart.name`. **관절 이름이면 관절을 모는 channel 이다** — 아래 「관절」 |
221
- | `path` | `'translation' \| 'rotation' \| 'scale'` | 무엇을 움직이나 |
222
- | `pivot` | `Vec3` | 회전 중심. `rotation` 에서만. 안 적으면 부품 제 중심 |
223
- | `interpolation` | `'linear' \| 'step'` | 안 적으면 `linear` |
224
- | `keys` | `Keyframe[]` | **둘 이상.** 하나는 움직임이 아니라 자세다 |
225
-
226
- #### `Keyframe`
227
-
228
- | 필드 | 타입 | 뜻 |
229
- | ------- | -------- | ------------------------------------------------------ |
230
- | `at` | `number` | 시각(초). 0 이상, **오름차순** |
231
- | `value` | `Vec3` | 경로마다 뜻이 다르다 — 아래 |
232
-
233
- ```
234
- translation 부품 기준 옮김
235
- rotation 오일러 도(°) transform.rotation 과 같은 약속
236
- scale 배율. 1 이 제 크기
237
- ```
238
-
239
- #### glTF 와 다른 자리 다섯
240
-
241
- | 여기 | glTF | 왜 |
242
- | --- | --- | --- |
243
- | `target` 이 부품 **이름** | 노드 인덱스 | 이 형식은 처음부터 이름으로 가리킨다 |
244
- | 회전 키가 **오일러 도** | 쿼터니언 | 손으로 적는 형식이다. `transform.rotation` 과 같은 약속 |
245
- | `pivot` 이 채널에 | 부모 노드를 끼운다 | 부품이 겹치지 않아 끼울 노드가 없다. 부모에 대해 도는 부품은 관절(`joints`)이 그 노드다 |
246
- | `weights` 없음 | 있음 | 모프 타깃이 없다 |
247
- | 값이 모는 곡선을 따로 갖는다(`parameters`) | 전부 애니메이션 | 트윈은 타임라인을 재생하지 않는다 |
248
- | 채널·샘플러를 합침 | 나뉨 | 나눔은 정점 데이터를 아끼는 장치다. 손으로 적을 때는 한 겹일 뿐 |
249
-
250
- **내보낼 때 주의**: 회전을 쿼터니언으로 바꾸면 **360° 한 바퀴가 항등이 되어** 키 둘로는
251
- 표현이 안 된다. 180° 넘는 구간은 내보내는 쪽이 쪼개야 한다 — 표준 익스포터가 하는 일이다.
252
-
253
- 그래서 「진동」·「맥동」은 별도 종류가 아니라 **왕복하는 키프레임**이다.
254
-
255
- ```js
256
- // 15° 씩 흔들린다
257
- { name: 'shake', channels: [{ target: 'deck', path: 'rotation', keys: [
258
- { at: 0, value: { x: 0, y: 0, z: -15 } },
259
- { at: 0.25, value: { x: 0, y: 0, z: 15 } },
260
- { at: 0.5, value: { x: 0, y: 0, z: -15 } }
261
- ]}]}
262
- ```
263
-
264
- #### `FigureParameter` — 값이 자세를 만드는 것
265
-
266
- 호이스트가 얼마나 내려왔나, 포크가 얼마나 올라갔나, 닙이 얼마나 닫혔나. 트윈은 「클립을
267
- 재생하라」가 아니라 **「지금 1200mm 내려와 있다」**를 준다.
268
-
269
- | 필드 | 타입 | 뜻 |
270
- | --------- | ---------------- | ----------------------------------------------------------- |
271
- | `name` | `string` | **저장되는 식별자.** 인스턴스가 이 이름으로 값을 준다 |
272
- | `label` | `string` | 화면에 보일 이름. 없으면 `name` |
273
- | `range` | `ParameterRange` | 주고받는 물리량. `{ unit, min, max }` |
274
- | `default` | `number` | 아무도 안 줬을 때. 없으면 `range.min` |
275
- | `clip` | `{ duration?, channels }` | 자세. `AnimationClip` 과 같은 모양이되 이름이 없고, 키의 `at` 이 값의 구간이다 |
276
-
277
- **`range` 가 이 기능의 요점이다.** 곡선의 값 축은 0~1 인데(glTF 의 morph target `weights`,
278
- Unity 의 파라미터와 같다), 저작자와 인스턴스가 주고받는 것은 **mm 다.** 「길이값을 변수로
279
- 노출한다」는 그 사이가 있어야 성립한다. 런타임이 맵한다.
280
-
281
- `unit` 은 형식이 해석하지 않는다. 화면이 보이고 인스턴스가 읽는 낱말이다 — `mm` · `deg` ·
282
- `%` 처럼. 형식이 단위를 판정하려 들면 단위 표를 형식이 갖게 되고, 그것은 형식의 일이 아니다.
283
-
284
- **키의 `at` 은 초가 아니라 값의 구간이다 — `0` 이상 `1` 이하.** `0` 이 `range.min`, `1` 이
285
- `range.max` 의 자세다. `1` 을 넘는 키는 어떤 값으로도 닿지 않으므로 검증이 거절한다. 키가 구간을
286
- 다 덮을 필요는 없다 — 첫 키 앞과 마지막 키 뒤는 끝 키의 자세에 머문다(glTF 의 범위 밖 규칙과 같다).
287
-
288
- **전이 시간은 `clip.duration` 이다.** `min` 에서 `max` 까지 전 구간을 움직이는 **초**이고, 곧 그
289
- 기계의 속도다. 값이 v₁ 에서 v₂ 로 바뀌면 `|v₂−v₁| × duration` 동안 직선으로 간다. **ease 는 없다.
290
- 기계는 연출하지 않는다.** `duration` 이 없거나 `0` 이면 즉시다. 같은 도형이라도 설비마다 속도가
291
- 다르면 인스턴스가 `state.parameterDurations` 로 덮는다.
292
-
293
- 키가 값 축에 있고 시간은 `duration` 한 칸에 있으므로 glTF 와는 손실 없이 오간다.
294
-
295
- ```
296
- 내보낼 때 glTF 키 시각 = at × (duration || 1)
297
- 받아들일 때 가장 늦은 키 시각으로 나눠 at 을 만들고, 그 시각을 duration 으로 둔다
298
- ```
299
-
300
- ```js
301
- // 2.5 초에 걸쳐 1800mm 를 오르내리는 호이스트
302
- {
303
- name: 'hoist',
304
- label: '갈고리 높이',
305
- range: { unit: 'mm', min: 0, max: 1800 },
306
- default: 0,
307
- clip: { duration: 2.5, channels: [{ target: 'hook', path: 'translation', keys: [
308
- { at: 0, value: { x: 0, y: 0, z: 0 } },
309
- { at: 1, value: { x: 0, y: -1800, z: 0 } }
310
- ]}]}
311
- }
312
- ```
313
-
314
- clip 의 `drive` 칸은 없다. 옛 원본의 `drive: 'hold'` 는 판 2 로 옮길 때 파라미터로 함께 옮겼고, 형식은 그 칸을
315
- 거절한다.
316
-
317
- 이름은 **두 목록이 나눠 쓴다.** 인스턴스는 이름 하나로 값을 주므로 clip 과 파라미터가 같은
318
- 이름을 가지면 어느 쪽에 준 것인지 알 수 없고, 검증이 그것을 막는다.
319
-
320
- ### 관절 — `FigureSource.joints`
321
-
322
- > ADR-0066 (`operato-twin/design/04-decisions.md`)
323
-
324
- 로봇 팔의 어깨가 돌면 팔꿈치 · 손목 · 집게가 **함께** 돈다. 부품 하나를 `pivot` 중심으로 돌리는 channel 로는
325
- 이것을 적을 수 없다. 부품마다 곡선이 따로라 어깨와 팔꿈치가 따로 움직일 때 회전이 합쳐지지 않는다. 관절이 그
326
- 부모 · 자식을 준다. URDF 의 link · joint 트리, glTF 의 노드 계층과 같은 모양이다.
327
-
328
- **부모는 부품이 말하고, 움직임은 관절이 말한다.**
329
-
330
- - `FigurePart.parent` 가 「이 부품은 어느 부품에 붙어 있나」다. 부모를 적는 자리는 여기 하나다.
331
- - 관절은 「그 부품(`child`)이 부모에 대해 어떻게 움직이나」만 말한다. 관절에는 `parent` 칸이 없다. 관절의 부모는
332
- `child` 부품의 `parent` 다.
333
- - 관절 없이 `parent` 만 있는 부품은 부모와 **함께** 움직인다(고정 부착). URDF 로 내보내면 한 link 의 visual 여럿이 된다.
334
- - 부모를 따라가면 figure 의 틀에 닿아야 한다. 되돌아오면 `parent-cycle` 이다. 한 부품에 관절은 하나다.
335
-
336
- | 필드 | 타입 | 뜻 |
337
- | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------- |
338
- | `name` | `string` | **저장되는 식별자.** channel 이 이 이름으로 관절을 몬다. 부품 이름과 겹칠 수 없다 |
339
- | `child` | `string` | 움직이는 부품 |
340
- | `type` | `'revolute' \| 'prismatic' \| 'continuous'` | 도는가(한계 안), 미끄러지는가(한계 안), 끝없이 도는가 |
341
- | `origin` | `Vec3` | 돌거나 미끄러지는 기준점. figure 좌표(판 2 — y 는 상자 밑면에서) |
342
- | `axis` | `Vec3` | 방향. 회전은 오른손 법칙. 길이는 상관없고 0 은 안 된다 |
343
- | `limits` | `{ min, max }` | revolute 는 도, prismatic 은 mm. **0 을 품어야 한다.** continuous 에는 적지 않는다 |
344
-
345
- **그린 자세가 관절 값 0 이다.** 모든 부품의 `transform` 은 지금처럼 휴지 자세를 figure 좌표로 적는다. 계층은
346
- compile 이 만든다. 그래서 `limits` 는 0 을 품어야 한다 — 그린 자세가 그 기계가 취할 수 없는 자세일 수는 없다.
347
-
348
- #### 관절을 모는 channel
349
-
350
- 값의 문은 `parameters` 하나다(ADR-0051). `parameters[].clip.channels` 의 `target` 이 관절 이름을 가리키면
351
- 그 channel 은 관절을 몬다. 끝없이 도는 관절은 지금처럼 loop clip(`animations`)으로 몰 수 있다.
352
-
353
- - **`path` 도 `pivot` 도 적지 않는다.** 축과 중심은 관절이 이미 말했다.
354
- - **키의 값은 관절의 좌표 하나다** — revolute · continuous 는 도, prismatic 은 mm. `limits` 밖이면 `out-of-range`.
355
- - 노드의 회전(glTF 로 내보낼 때의 quaternion 포함)은 관절의 축에서 만든다.
356
- - 부품을 모는 channel(`path` · `pivot`)은 그대로 쓴다. 그 부품이 관절 아래에 있으면 **그 관절의 틀 안에서** 움직인다.
357
-
358
- ```js
359
- // 팔꿈치를 -150° ~ 150° 로, 3 초에 전 구간
360
- { name: 'elbow-flex', range: { unit: 'deg', min: -150, max: 150 },
361
- clip: { duration: 3, channels: [{ target: 'elbow-flex', keys: [{ at: 0, value: -150 }, { at: 1, value: 150 }] }] } }
362
- ```
363
-
364
- #### 묶음 · 닻 · 크기 반응
365
-
366
- - **묶음은 (관절의 틀, 재질)로 합친다.** 합친 geometry 는 변환을 하나만 받으므로 다른 틀에 있는 부품은 합칠 수
367
- 없다. 부품의 틀은 부모를 따라 올라가며 처음 만나는 관절이다(자기 자신이 `child` 면 그 관절). 게이트의 draw call 셈도
368
- 같은 함수(`mergeKeyOf`)를 쓴다.
369
- - **slot · port 의 닻은 자기 부품의 틀을 따라 움직인다**(`BlueprintAnchor.joint`).
370
- - **크기 반응은 휴지 자세에서 먼저 푼다.** 관절의 `origin` 은 `child` 의 부모 부품이 받는 크기 반응을 따라 옮겨진다
371
- (`jointOriginPosition`). 관절 노드에는 배율을 걸지 않는다 — 축마다 배율이 다른 인스턴스에서 자식을 돌려도 찌그러지지
372
- 않는다.
373
- - **옮김의 크기는 배율을 따른다.** 부품을 옮기는 channel(`translation`)은 인스턴스 배율이 걸린 틀 안에서 더해지므로
374
- 축마다 인스턴스 배율만큼 늘어난다 — 높이를 두 배로 놓은 호이스트의 200mm 는 400mm 를 간다(things-scene
375
- `FigureRealObject` 가 `object3d.scale` 에 인스턴스 배율을 걸고 그 안에서 옮긴다). prismatic 관절의 행정도 이것과
376
- **같게** 한다: 관절의 부모 틀에서 `axis × 값` 의 각 축 성분에 그 축의 인스턴스 배율을 곱한다. 같은 figure 안에서
377
- 옮김의 거동이 둘이면 안 되기 때문이다(ADR-0066 결정 3).
378
-
379
- #### 짓지 않은 것
380
-
381
- IK(`ikChains`) · 감쇠 스프링 · `coupling` · `maxSpeed` · `acceleration` · `smoothing` 은 형식에 없다. 소비처가 생기면
382
- 그때 따로 판정한다. 관절이 있는지는 형식에서 읽히므로 능력 어휘(`capabilities`)에 관절용 이름을 두지 않는다.
383
-
384
- ### `FigureSource.capabilities` — 이 도형이 흐름에서 무엇을 하나
385
-
386
- 파렛트는 물건을 **담고**, 컨베이어는 **흘리고**, 신호탑은 아무것도 담지 않는다. 그리는
387
- 것만으로는 컨베이어가 컨베이어가 아니다.
388
-
389
- ```
390
- Capacity 자리를 갖는다
391
- Holdable 담긴 것을 함께 들고 그린다
392
- Transferable 자리를 잡아 두고 넘겨받는다 — Capacity 나 FlowNode 가 함께 있어야 한다
393
- FlowNode 흐름의 마디다. 위의 셋을 스스로 포함한다
394
- ```
395
-
396
- **닫힌 목록이다**(`FIGURE_CAPABILITIES`). 한동안 이 목록이 씬에만 있어서 검증이 이름을 못
397
- 봤고, AI 가 낸 `capabilities: ['hoist']` 가 **검증을 지나고 발행을 지나** 렌더링에서 죽었다.
398
- 게이트는 사실이 사는 곳에 있어야 한다.
399
-
400
- `hoist` 는 능력이 아니다. 능력은 **흐름에서 무엇을 하는가**이고, 호이스트의 연장은
401
- **자세를 어떻게 구동하는가**(위의 파라미터)다. 다른 축이다.
402
-
403
- ### `LabelSpec`
404
-
405
- | 필드 | 타입 | 뜻 |
406
- | -------- | ----------- | --------------------------------------------- |
407
- | `name` | `string` | 라벨 이름 |
408
- | `source` | `string` | 태그의 어느 값을 보이나 |
409
- | `when` | `LabelWhen` | `always` · `zoomed` · `selected` · `abnormal` |
410
-
411
- 표현 수단(sprite · overlay)은 런타임이 고른다. `always` 는 대량 배치에서 비싸다 —
412
- 하나를 넘으면 위반이다.
413
-
414
- ### `PartCapability` — 부품의 닻(능력)
415
-
416
- 그려지지 않고 자리(`slot`) 또는 문(`port`)으로 컴파일되는 부품의 능력 선언.
417
-
418
- | 필드 | 타입 | 필수 | 뜻 |
419
- | ----------- | ------------ | ---- | ------------------------------------------------------------------------------------------------------------- |
420
- | `roles` | `PartRole[]` | ✓ | 역할 목록 (`slot` · `port-in` · `port-out`). 하나 이상이어야 한다 |
421
- | `accepts` | `string[]` | | 수용 가능한 자산 타입 목록. 비었으면 모든 타입 수용 (`SlotDef.allowedTypes`) |
422
- | `capacity` | `number` | | 고정 수용 칸 수. 1 이상의 정수. `sizing: 'repeat'`와 함께 쓰지 않는다 |
423
- | `direction` | `Vec3` | | 포트의 법선/진출입 진행 방향 벡터. 기본값은 부품 로컬 +z `{ x: 0, y: 0, z: 1 }` (`transform.rotation`으로 회전) |
424
-
425
- ### `SizingRule`
426
-
427
- 인스턴스 크기가 바뀔 때 부품이 어떻게 변하는가.
428
-
429
- | 값 | 뜻 | 예 |
430
- | --------- | -------------------------------------------------- | -------------------------- |
431
- | `scale` | 비례 확대 | 탱크 몸체 같은 단일 덩어리 |
432
- | `fixed` | 크기는 그대로. **붙잡은 면에서 잰 거리**를 지킨다 | 모터 · 계기 · 노즐 |
433
- | `stretch` | 가장 긴 축이 **양쪽 사이를 채운다** | 프레임 · 벨트 · 레일 |
434
- | `repeat` | **개수가 늘어난다** — 새 geometry 가 생기지 않는다 | 롤러 · 선반 단 |
435
-
436
- ### `PartShape.hollow` — 속을 판다
437
-
438
- 트레이 · 토트 · 통 · 케이스 · 프레임이 전부 이 모양이다. 없을 때는 바닥판 하나에 벽 넷을
439
- 세워 만들었는데, 부품이 다섯인 것보다 나쁜 것은 **모양이 안 맞는다**는 것이었다 —
440
- 셀트레이의 바닥판은 모서리를 24 로 깎았고 벽 넷은 저마다 12 로 깎은 판이라, 네 귀퉁이에서
441
- 둥근 것 둘이 어긋나게 만났다.
442
-
443
- | 필드 | 뜻 |
444
- | ------- | ---------------------------------------------------------- |
445
- | `wall` | 벽 두께. 안쪽 테는 바깥 테에서 이만큼 들어온다 |
446
- | `floor` | 바닥 두께. 없으면 벽과 같다. **`0` 이면 위아래가 다 뚫린다** |
447
-
448
- `size` 는 **바깥 치수**다. 프리미티브를 새로 만들지 않은 것은 「속을 판다」가 상자만의
449
- 성질이 아니기 때문이다 — `polygon` 을 파면 U 자 가드 · ㄷ 자 프레임이 같은 규칙으로
450
- 나온다. 다만 **오목한 단면은 아직 못 판다**(`hollow-not-convex`).
451
-
452
- 삼각형은 이렇게 든다. 재 본 값이다.
453
-
454
- ```
455
- rect 각짐 12 cube 와 같다 — 민다고 비싸지지 않는다
456
- rect round=24 140 모서리마다 8 점으로 뽑으므로 테가 36 점이 된다
457
- 속 판 rect round=24 160 구멍은 +20. 고리의 캡이 통짜 캡보다 싸다
458
- ```
459
-
460
- **비싼 것은 미는 것이 아니라 라운드다.**
461
-
462
- ### `keepRound` — 원통을 원통으로 둘까
463
-
464
- 인스턴스가 축마다 다른 배율을 받으면 원기둥은 타원 기둥이 된다. AGV 를 길게 늘이면
465
- 바퀴가 찌그러지고, 크레들을 높이면 롤이 납작해진다. 기본은 **지킨다**이다 — 원기둥이라고
466
- 적어 놓고 원기둥이 아닌 것을 그리지 않는다.
467
-
468
- 지키는 방식은 **단면의 두 축이 같은 배율을 받는 것**이고, 그 배율은 두 배율의
469
- **기하평균**이다. 크기를 얼리는 것이 아니다 — 인스턴스가 커지면 원통도 커지되 원을
470
- 지킨다. 작은 쪽에 맞추면 한 축만 늘인 인스턴스에서 지름이 묶여 **형태가 아니라 크기**를
471
- 지키게 된다.
472
-
473
- #### 비용
474
-
475
- 합친 형상은 변환을 하나만 받는다. 원을 지키려면 제 변환이 있어야 하므로 **지키는 곡면
476
- 부품 하나가 draw call 하나**다.
477
-
478
- ```
479
- 표본 열넷의 draw call 139 → 168 (+21%)
480
- AGV 3 → 12 바퀴 · 기둥 · 램프가 다 원통이다
481
- 작업자 4 → 13 팔다리가 원통이다
482
- 전극 롤 크래들 8 → 17
483
- ```
484
-
485
- **형상은 계속 나눠 쓴다.** 따로 서는 것과 정점 버퍼를 나눠 쓰는 것은 다른 이야기다 —
486
- 같은 규격 롤러 여덟은 묶음 여덟이어도 geometry 는 하나다. 그래서 이 비용은 인스턴싱이
487
- 가장 잘 먹는 모양이기도 하다.
488
-
489
- #### 스타일 밸런스
490
-
491
- 끌 만한 것과 아닌 것이 갈린다.
492
-
493
- ```
494
- 켜 둔다 화면의 주역 롤 · 바퀴 · 탱크 · 기둥 · 파이프
495
- 꺼도 된다 찌그러져도 모를 것 볼트 머리 · 작은 스터드 · 발 받침
496
- ```
497
-
498
- 로우폴리에서 지름 100 아래의 원통은 8 분할이면 이미 팔각형이라, 조금 찌그러져도 읽는
499
- 사람이 눈치채지 못한다. 그런 것을 끄면 그 묶음이 다시 합쳐진다.
500
-
501
- ### `PartAnchor` — 어느 면을 붙잡나
502
-
503
- `sizing` 은 부품의 **크기**가 인스턴스를 어떻게 따라가는지만 말했다. **자리**는 언제나
504
- 비례였고, 그래서 시그널 타워를 3 배로 세우면 기둥은 늘어나는데 램프들이 기둥 위 공중으로
505
- 흩어졌다. 램프는 상자 안 어디쯤이 아니라 **기둥 꼭대기**에 붙어 있다.
506
-
507
- | 값 | 뜻 |
508
- | -------- | ------------------------------------------------------ |
509
- | `scale` | **아무 면도 안 붙잡는다.** 상자와 함께 비례로 커진다 |
510
- | `min` | 아래(왼쪽 · 뒤) 면에서 잰 거리를 지킨다 |
511
- | `center` | 자리는 비례로 따라가고 **크기는 지킨다** |
512
- | `max` | 위(오른쪽 · 앞) 면에서 잰 거리를 지킨다 |
513
- | `span` | **양쪽을 다 붙잡는다.** 가운데가 늘어나 사이를 채운다 |
514
-
515
- `repeat` 의 축에서는 **줄이 어떻게 되는가**를 말한다. 같은 「반복」이 두 가지다 —
516
- 컨베이어의 롤러는 길어지면 개수가 늘고(`center`), 셀트레이의 셀은 커지면 셀이 커진다
517
- (`scale`). 개수는 그 트레이가 몇 셀짜리냐이지 화면에 얼마나 크게 그렸느냐가 아니다.
518
-
519
- 붙잡는 면의 **수**로 읽으면 하나의 규칙이다 — 0 개면 상자와 함께 커지고(`sizing: 'scale'`),
520
- 1 개면 그 면에서 잰 거리를 지키고, 2 개면 사이를 채운다.
521
-
522
- **적지 않으면 `sizing` 이 정한다.** `'fixed'` 는 가까운 면(부품의 면에서 상자의 면까지
523
- 재서 짧은 쪽), `'stretch'` 는 가장 긴 축이 `span` 이고 나머지는 가까운 면, `'repeat'` 는
524
- 늘어나는 축이 `center` 이고 나머지는 가까운 면이다. 표본 열넷 중 어느 것도 이 값을 적지
525
- 않고 제대로 서므로, 기본값이 사람이 이미 뜻하는 것이라고 볼 만하다.
526
-
527
- **`sizing` 하나로는 못 하는 말이 있어서 축별로 둔다.** 컨베이어의 데크가 그렇다 —
528
- 길이(x)와 폭(z)은 상자를 채우고 두께(y)는 24 mm 그대로여야 한다.
529
-
530
- ```
531
- sizing: 'fixed', anchor: { x: 'span', y: 'max', z: 'span' }
532
- ```
533
-
534
- **적어 두면 `sizing` 을 이긴다 — `'scale'` 이어도 그렇다.** `'scale'` 은 「적지 않았을 때
535
- 그 축이 상자와 함께 커진다」는 뜻이고, 축 하나에 `max` 를 적으면 그 축에서는 위 면에서
536
- 잰 거리를 지킨다. 두 축은 함께 키우고 한 축만 붙잡는 것이 정상적인 저작이다.
537
-
538
- ---
539
-
540
- ## `FigureBlueprint` — 청사진
541
-
542
- `compile(source)` 가 만든다. 저장하지 않고 원본 데이터에서 언제든 다시 만든다.
543
-
544
- | 필드 | 타입 | 뜻 |
545
- | -------------------------- | ----------------------- | ----------------------------------------- |
546
- | `version` | `number` | `BLUEPRINT_VERSION`. 청사진 자신의 판이다. 컴파일 결과의 모양이 바뀌면 오른다 |
547
- | `sourceVersion` | `number` | 원본이 적은 `FigureSource.version`. 소비처는 청사진만 받으므로 **원본의 판을 여기서 안다.** 원본에 없으면 `FIGURE_SOURCE_VERSION`(1)이 채워져 언제나 수다 |
548
- | `type` · `base` | | 원본 데이터에서 그대로 |
549
- | `budget` | `{ triangles, groups }` | 예산. `groups` 가 **재는 값**이다 |
550
- | `groups` | `BlueprintGroup[]` | 재질별로 병합된 묶음 |
551
- | `animations` | `BlueprintClip[]` | 움직임. 원본에 적은 것 그대로 |
552
- | `labels` | `BlueprintLabel[]` | 라벨. `at` 이 부품 이름 |
553
- | `joints` | `BlueprintJoint[]` | 관절. **부모가 먼저 온다.** 원본에 관절이 없으면 칸 자체가 없다 |
554
- | `violations` | `Violation[]` | 정책을 넘은 것. 저작 화면이 보인다 |
555
-
556
- ### `BlueprintGroup` — 병합 단위 = draw call 하나
557
-
558
- | 필드 | 뜻 |
559
- | ------------- | --------------------------------------------------- |
560
- | `materialKey` | MaterialBank 키 |
561
- | `material` | 원본 데이터의 `PartMaterial` 그대로 |
562
- | `materialSlot` | 재질 슬롯 |
563
- | `animated` | clip 이나 parameter 가 움직이는 부품이 든 그룹인지. **그러면 부품을 하나만 갖는다** |
564
- | `members` | `BlueprintMember[]` |
565
- | `joint` | 이 묶음이 움직이는 관절의 틀. 없으면 figure 의 틀 |
566
-
567
- ### `BlueprintJoint` — 관절 노드
568
-
569
- | 필드 | 뜻 |
570
- | ------------------ | --------------------------------------------------------------------------- |
571
- | `name` · `type` · `child` · `limits` | 원본에서 |
572
- | `origin` | 휴지 자세의 기준점. **상자 중심 좌표** — 청사진의 다른 자리와 같다 |
573
- | `axis` | 길이 1 로 맞춘 방향 |
574
- | `parent` | 이 관절이 놓이는 관절의 틀. 없으면 figure 의 틀 |
575
- | `attach` | `child` 의 부모 부품. 크기 반응에서 `origin` 이 이 부품을 따라간다. 없으면 figure 의 틀에 붙은 관절이다 |
576
-
577
- ### `BlueprintMember` — 묶음 안의 부품
578
-
579
- | 필드 | 뜻 |
580
- | ------------------------------------------------------------------ | ----------------------------------------------------------------- |
581
- | `name` · `primitive` · `transform` · `shape` · `sizing` · `repeat` | 원본 데이터에서 |
582
- | `segments` | 곡면 분할 수. **2D 톱뷰를 3D 와 같은 각으로 근사하려면 필요하다** |
583
- | `geometryKey` | GeometryBank 키. 실체가 아니라 참조다 |
584
- | `triangles` | 이 부품의 삼각형 수 **추정** |
585
-
586
- ---
587
-
588
- ## 키
589
-
590
- | 함수 | 식 |
591
- | --------------- | --------------------------------------------- |
592
- | `geometryKeyOf` | `primitive\|WxHxD[\|s분할][\|r반경][\|p점수]` |
593
- | `materialKeyOf` | `token\|preset\|f\|t` (`-` 는 없음) |
594
- | `groupKeyOf` | `materialKey\|\|materialSlot` |
595
-
596
- **geometry 키에 치수가 들어간다.** 경로에서 오는 형상은 단위로 정규화하면 모서리 반경
597
- 비율이 망가지므로 치수를 굽고, 대신 같은 치수끼리만 공유한다. **자리는 키에 들지
598
- 않는다** — 어디 있든 같은 형상이다.
599
-
600
- ## 한도와 프리셋
601
-
602
- | | 값 | 근거 |
603
- | ----------------------------- | ---------------- | -------------------------------------------------------------------- |
604
- | `SEGMENT_PRESETS` | 8 · 12 · 16 · 24 · 32 · 48 | 좁게 두면 단위 곡면이 그만큼만 창고에 남아 전체 자산이 나눠 쓴다 |
605
- | `LIMITS.minSegments` | 3 | 2 이하는 면이 생기지 않는다 |
606
- | `LIMITS.materialGroups` | 3 | 자산당 draw call |
607
- | `LIMITS.independentParts` | 12 | 따로 변환되는 부품마다 묶음이 늘고, 그만큼 한 화면에 세울 대수가 준다 |
608
- | `LIMITS.transparentMaterials` | 1 | 투명은 별도 정렬 경로 |
609
- | `LIMITS.surfaceTextures` | 1 | 무늬·그라디언트는 재질을 나눠 쓸 수 없다. 라벨 판 한 군데쯤은 봐준다 |
610
- | `PART_LIMIT` | S 3 · M 8 · L 20 | 등급별 부품 수 |
611
-
612
- **미감을 위한 제약이 그대로 성능의 근거다.** 격자 스냅 · 분할 프리셋 · 색 팔레트 ·
613
- 디테일 등급은 「감각적인 로우폴리」를 위해 둔 규칙인데, 같은 규칙이 geometry 키와
614
- material 키를 줄인다.
615
-
616
- ---
617
-
618
- ## 원가와 점수 — 저작자가 보는 지표
619
-
620
- `costOf(blueprint, instances = 100)` 는 **인스턴스가 늘 때 무엇이 늘고 무엇이 그대로
621
- 인지**를 낸다. 그대로인 것이 곧 재활용되는 것이다.
622
-
623
- | 필드 | 뜻 |
624
- | -------------------------------- | ----------------------------------------------------- |
625
- | `triangles` · `groups` | Figure 하나의 삼각형 수와 묶음 수 |
626
- | `distinctShapes` | 서로 다른 형상 수. 부품 자리보다 적으면 그만큼 되쓴다 |
627
- | `distinctMaterials` | 서로 다른 재질 수 |
628
- | `shapeReuse` | `distinctShapes / parts`. 1 이면 되쓰기 없음 |
629
- | `at.triangles` · `at.drawCalls` | **인스턴스에 비례해 오른다** |
630
- | `at.geometries` · `at.materials` | **인스턴스와 무관하다** |
631
- | `at.drawCallsIfInstanced` | `InstancedMesh` 를 쓰면 도달할 draw call |
632
-
633
- `describeCost(cost)` 는 같은 값을 사람이 읽는 줄로 옮긴다. 값을 다시 계산하지 않는다.
634
-
635
- ### 점수 — 근거는 선언된 한도뿐이다
636
-
637
- `scoreOf(blueprint)` 가 낸다. **새로 지어낸 기준이 하나도 없다** — 점수를 매기려고
638
- 만든 수가 섞이면 「왜 그 점수냐」에 답할 수 없기 때문이다.
639
-
640
- | 한도 | 값 | 무엇을 지키나 |
641
- | ------------- | ----------------------------- | ------------------------------------------------ |
642
- | `groups` | `LIMITS.materialGroups` | 인스턴스 하나마다 치르는 draw call |
643
- | `independent` | `LIMITS.independentParts` | 따로 변환되는 부품마다 묶음이 늘고, 그만큼 한 화면에 세울 대수가 준다 |
644
- | `transparent` | `LIMITS.transparentMaterials` | 투명은 별도 정렬 경로 |
645
- | `parts` | `PART_LIMIT[detailLevel]` | 등급이 정한 상한. 등급이 없으면 이 항목이 빠진다 |
646
-
647
- - `score` — **한도에 가장 가까운 항목에 남은 여유**(0–100). 평균이 아니다. 넷 중 하나만 꽉
648
- 차 있어도 대량 배치에서는 그것이 병목이 되므로, 평균을 내면 병목이 여유에 묻힌다.
649
- - `grade` — **A** 여유 절반 이상 · **B** 4분의 1 이상 · **C** 한도 안 · **D** 한도 초과.
650
- D 는 곧 위반이 있다는 뜻이고 사유는 `blueprint.violations` 에 있다.
651
- - `tightest` — 어느 한도가 가장 가까운가. **여기부터 고치면 된다.**
652
- - `reuse` — 형상 되쓰기. **점수에 넣지 않는다.** 되쓰기는 많을수록 좋지만 한도가
653
- 없다. 점수에 섞으면 「형상을 억지로 같게 만들라」는 신호가 되어 저작을 왜곡한다.
654
-
655
- ### 추이
656
-
657
- `costCurve(blueprint, { from, to })` 는 1 개에서 10,000 개까지(기본) 자릿수마다 1·2·5
658
- 간격으로 훑어 점들을 낸다. `costCurveSvg` 는 그것을 SVG 문자열로 낸다 — 의존을 만들지
659
- 않으려고 문자열이며, 축과 글자는 `currentColor` 라 밝은 테마·어두운 테마 양쪽에서
660
- 읽힌다.
661
-
662
- 두 축 모두 로그다. 선형으로 그리면 10,000 쪽 값이 화면을 다 먹어, 정작 보여 줄 것 —
663
- **오르는 선과 평평한 선의 대비** — 이 안 보인다.
664
-
665
- ---
666
-
667
- ## 구현 상태 — 선언됐으나 아직 동작하지 않는 것
668
-
669
- **이 절이 이 문서에서 가장 중요하다.** 형식에 자리가 있다고 해서 그 자리가 동작하는
670
- 것은 아니다. 아래는 원본 데이터와 청사진에 함께 나가지만 **읽어서 무언가 하는 코드가 아직
671
- 없는** 것들이다.
672
-
673
- | 선언 | 검증 | 청사진에 실림 | 읽는 곳 |
674
- | --------------------- | ---- | ------------- | -------------------------------------------------- |
675
- | `labels` | ✓ | ✓ | **없음** — 그리는 곳이 없다 |
676
- | `styleKit` | ✓ | — | **없음** |
677
- | `material.surface` | ✓ | ✓ | **없음** — 셈에는 들어가지만 그리는 곳이 아직 없다 |
678
-
679
- 이 표는 기능이 붙을 때마다 함께 고친다. 「선언은 있는데 길이 없다」를 문서가 감추면
680
- 읽는 사람이 없는 기능을 있다고 믿는다.
681
-
682
- **반대 방향도 위험하다.** 이 표는 한동안 `sizing`·`repeat`·`anchor`·`transform.rotation` 을
683
- 「읽는 곳 없음」으로 두고 있었고, 그 넷은 그 사이에 전부 구현되었다(2D 톱뷰·3D·발행 게이트가
684
- 모두 읽는다. 세는 법은 아래).
685
-
686
- ```
687
- sizing things-scene/src/figure/{figure-instance, figure-real-object}.ts
688
- repeat 같은 둘 + figure-part-component · figure-capability
689
- anchor figure-sizing.ts 를 지나 위의 전부
690
- rotation figure-real-object.ts · figure-sizing.ts
691
- animations figure-real-object.ts (clip 을 실제로 돌린다)
692
- ```
693
-
694
- 낡은 「없음」은 낡은 「있음」보다 조용히 해를 끼친다 — 저작 화면의 anchor 편집기가 아무 일도
695
- 하지 않는 칸을 편집하는 것 아니냐는 의심을 이 표가 만들었다.
696
-
697
- ### 폴리곤 수
698
-
699
- `trianglesOf(part)` 와 `blueprint.budget.triangles` 는 **식으로 낸 추정치**다.
700
- 실제 병합 geometry 의 삼각형 수와 맞는지는 **아직 확인되지 않았다** — 특히
701
- `polygon` 의 `2(n−2) + 2n` 은 `ExtrudeGeometry` 의 실제 cap 삼각분할과 다를 수 있다.
702
-
703
- Figure 한 종의 값과 인스턴스별 추이는 `costOf` · `costCurve` 로 낸다. **도면 전체
704
- 합계**(여러 타입이 섞인 판)는 내지 않는다 — 필요한 것이 Figure 단위라서 범위에서 뺐다.