@hatiolab/figure-model 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +163 -0
- package/dist/blueprint.d.ts +32 -0
- package/dist/blueprint.d.ts.map +1 -0
- package/dist/blueprint.js +163 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/cost-chart.d.ts +35 -0
- package/dist/cost-chart.d.ts.map +1 -0
- package/dist/cost-chart.js +150 -0
- package/dist/cost-chart.js.map +1 -0
- package/dist/cost.d.ts +160 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +170 -0
- package/dist/cost.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +38 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +57 -0
- package/dist/keys.js.map +1 -0
- package/dist/types.d.ts +360 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +162 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +13 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +327 -0
- package/dist/validate.js.map +1 -0
- package/docs/design.md +192 -0
- package/docs/format-survey.md +108 -0
- package/docs/format.md +295 -0
- package/docs/validation.md +159 -0
- package/package.json +80 -0
package/docs/format.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
things-scene 의 2D·3D 규약과 같다.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
x 오른쪽
|
|
18
|
+
y 아래 ← 2D 캔버스와 같은 방향
|
|
19
|
+
z 위
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**`height` 는 평면상의 세로이고, 높이는 `depth` 다.** 2D 로 그린 것을 위로 미는
|
|
23
|
+
구조라서 그렇게 되어 있다. 3D 에서 세울 때 `depth` 가 three.js 의 Y 로 간다.
|
|
24
|
+
|
|
25
|
+
`transform.x` · `y` · `z` 는 **부품의 최소 모서리**다. 중심이 아니다. 기준은
|
|
26
|
+
`base` 상자의 (0,0,0). 중심이 아니라 모서리인 이유는 격자 스냅 때문이다 — 저작이
|
|
27
|
+
그리드에 맞추는 것은 모서리이고, 중심 기준이면 홀수 크기 부품이 반 칸씩 밀린다.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## `FigureSource` — 정본
|
|
32
|
+
|
|
33
|
+
| 필드 | 타입 | 필수 | 뜻 |
|
|
34
|
+
| ------------- | -------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `type` | `string` | ✓ | 컴포넌트 타입 이름. **저장되는 식별자다** — 보드가 이 이름으로 청사진을 찾는다. 바꾸면 이미 놓인 인스턴스가 세워지지 않는다 |
|
|
36
|
+
| `base` | `{ width, height, depth }` | ✓ | 기준 치수. 배치 크기가 이 비율에서 나온다 |
|
|
37
|
+
| `parts` | `FigurePart[]` | ✓ | 부품. 하나는 있어야 한다 |
|
|
38
|
+
| `anchor` | `Vec3` | | 배치 기준점 |
|
|
39
|
+
| `detailLevel` | `'S' \| 'M' \| 'L'` | | 부품 수 상한을 정한다 (3 · 8 · 20) |
|
|
40
|
+
| `styleKit` | `string` | | StyleKit 이름 — 팔레트·분할 수·그리드·등급을 한 벌로 받는다 |
|
|
41
|
+
|
|
42
|
+
## `FigurePart` — 부품
|
|
43
|
+
|
|
44
|
+
| 필드 | 타입 | 필수 | 뜻 |
|
|
45
|
+
| ----------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `name` | `string` | ✓ | **저장되는 식별자다.** 바인딩·애니메이션·슬롯이 이 이름을 가리킨다. 바꾸면 인스턴스의 바인딩이 조용히 끊긴다. 한 Figure 안에서 겹칠 수 없다 |
|
|
47
|
+
| `primitive` | `PrimitiveKind` | ✓ | 아래 표 |
|
|
48
|
+
| `transform` | `PartTransform` | ✓ | 자리와 크기 |
|
|
49
|
+
| `material` | `PartMaterial` | ✓ | 색과 질감 |
|
|
50
|
+
| `segments` | `number` | | 곡면 분할 수. `cylinder` · `sphere` 만 |
|
|
51
|
+
| `shape` | `PartShape` | | 단면. `rect` · `polygon` 만 |
|
|
52
|
+
| `slot` | `'cap' \| 'side'` | | 상태 색이 연동될 재질 슬롯. 없으면 상태를 표현하지 않는다 |
|
|
53
|
+
| `sizing` | `SizingRule` | | 크기 반응 규칙. 기본 `'scale'` |
|
|
54
|
+
| `repeat` | `{ axis, gap }` | | `sizing` 이 `'repeat'` 일 때 필수 |
|
|
55
|
+
| `animation` | `AnimationPoint` | | 애니메이션 포인트 |
|
|
56
|
+
| `label` | `LabelSpec` | | 텍스트. 아래 표 |
|
|
57
|
+
|
|
58
|
+
### `PrimitiveKind`
|
|
59
|
+
|
|
60
|
+
| 값 | 계열 | 분할 수 | 단면 |
|
|
61
|
+
| ---------- | ------- | ------- | ------------------------ |
|
|
62
|
+
| `cube` | mesh | | |
|
|
63
|
+
| `wall` | mesh | | |
|
|
64
|
+
| `cylinder` | mesh | ✓ | |
|
|
65
|
+
| `sphere` | mesh | ✓ | |
|
|
66
|
+
| `rect` | extrude | | `shape.round` |
|
|
67
|
+
| `polygon` | extrude | | `shape.path` (3 점 이상) |
|
|
68
|
+
|
|
69
|
+
mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민다.
|
|
70
|
+
|
|
71
|
+
### `PartTransform`
|
|
72
|
+
|
|
73
|
+
| 필드 | 타입 | 필수 |
|
|
74
|
+
| ---------------------------- | --------------- | ---- |
|
|
75
|
+
| `x` · `y` · `z` | `number` | ✓ |
|
|
76
|
+
| `width` · `height` · `depth` | `number` | ✓ |
|
|
77
|
+
| `rotation` | `Partial<Vec3>` | |
|
|
78
|
+
|
|
79
|
+
`depth` 를 빼면 오류다. **0 으로 갈음하지 않는다** — 두께를 모르는 것과 두께가 0 인
|
|
80
|
+
것은 다르다.
|
|
81
|
+
|
|
82
|
+
### `PartMaterial`
|
|
83
|
+
|
|
84
|
+
| 필드 | 타입 | 뜻 |
|
|
85
|
+
| ------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `token` | `string` | 팔레트 토큰. 예: `'palette.primary'`. **값이 아니라 이름을 둔다** — 값을 구우면 테마를 바꿔도 안 따라오고, 저작한 자산 수백 개를 하나씩 고쳐야 한다 |
|
|
87
|
+
| `preset` | `MaterialPreset` | `default` · `metal` · `glass` · `plastic` · `wood` · `ceramic` · `rubber` |
|
|
88
|
+
| `flatShading` | `boolean` | 면마다 법선 하나. **로우폴리의 핵심 스위치다** — 분할이 낮은 원기둥은 smooth 에서 「덜 그린 원기둥」이고 flat 에서 「의도한 각기둥」이다 |
|
|
89
|
+
| `transparent` | `boolean` | 별도 정렬 경로를 탄다 |
|
|
90
|
+
| `surface` | `PartSurface` | **예외.** 아래 표 |
|
|
91
|
+
|
|
92
|
+
`token` 과 `preset` 중 하나는 있어야 한다.
|
|
93
|
+
|
|
94
|
+
#### 색은 팔레트 토큰 하나뿐이다 — 빠뜨린 것이 아니라 금지한 것이다
|
|
95
|
+
|
|
96
|
+
무늬·그라디언트를 담는 자리가 `PartMaterial` 에 없는 것은 아직 안 만들어서가 아니다.
|
|
97
|
+
두 이유에서 일부러 뺐다.
|
|
98
|
+
|
|
99
|
+
| | 이유 |
|
|
100
|
+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| 미감 | 색을 자유롭게 열면 로우폴리가 무너진다. 팔레트 토큰 몇 개로 제한하는 것이 「감각적인 로우폴리」의 첫 규칙이다 |
|
|
102
|
+
| 성능 | 단색이어야 **재질을 나눠 쓴다.** 무늬·그라디언트는 인스턴스마다 캔버스 텍스처가 필요해 재질이 인스턴스 수만큼 생긴다 — 캐리어 1만 개면 재질 1만 개 |
|
|
103
|
+
|
|
104
|
+
**두 이유가 같은 규칙을 가리킨다.**
|
|
105
|
+
|
|
106
|
+
### `PartSurface` — 예외 통로
|
|
107
|
+
|
|
108
|
+
정말 그것이어야 하는 자리(라벨 판, 표시창)에만 쓴다. **막지는 않지만 셈에 걸린다.**
|
|
109
|
+
|
|
110
|
+
| 필드 | 타입 | 뜻 |
|
|
111
|
+
| ------ | ------------------------- | ----------------------------------------------------------- |
|
|
112
|
+
| `kind` | `'gradient' \| 'pattern'` | 둘 다 인스턴스마다 캔버스 텍스처가 필요하다 |
|
|
113
|
+
| `ref` | `string` | 무엇을 쓰나. 텍스처 이름·이미지 키. 해석은 그리는 쪽이 한다 |
|
|
114
|
+
|
|
115
|
+
한 Figure 에 `LIMITS.surfaceTextures`(1) 개까지 봐준다. 넘으면 위반이고
|
|
116
|
+
원가표(`costOf`)의 재질 수와 점수(`scoreOf`)에 그대로 드러난다.
|
|
117
|
+
|
|
118
|
+
### `AnimationPoint`
|
|
119
|
+
|
|
120
|
+
| 필드 | 타입 | 뜻 |
|
|
121
|
+
| ------- | ------------------- | ----------------------------------------------------------------------------------- |
|
|
122
|
+
| `kind` | `AnimationKind` | `fade` · `heartbeat` · `moving` · `outline` · `rotation` · `vibration` · `waypoint` |
|
|
123
|
+
| `axis` | `'x' \| 'y' \| 'z'` | 회전은 필수 |
|
|
124
|
+
| `pivot` | `Vec3` | **회전은 필수.** 없으면 부품 자기 중심으로 돈다 |
|
|
125
|
+
|
|
126
|
+
### `LabelSpec`
|
|
127
|
+
|
|
128
|
+
| 필드 | 타입 | 뜻 |
|
|
129
|
+
| -------- | ----------- | --------------------------------------------- |
|
|
130
|
+
| `name` | `string` | 라벨 이름 |
|
|
131
|
+
| `source` | `string` | 태그의 어느 값을 보이나 |
|
|
132
|
+
| `when` | `LabelWhen` | `always` · `zoomed` · `selected` · `abnormal` |
|
|
133
|
+
|
|
134
|
+
표현 수단(sprite · overlay)은 런타임이 고른다. `always` 는 대량 배치에서 비싸다 —
|
|
135
|
+
하나를 넘으면 위반이다.
|
|
136
|
+
|
|
137
|
+
### `SizingRule`
|
|
138
|
+
|
|
139
|
+
인스턴스 크기가 바뀔 때 부품이 어떻게 변하는가.
|
|
140
|
+
|
|
141
|
+
| 값 | 뜻 | 예 |
|
|
142
|
+
| --------- | -------------------------------------------------- | -------------------------- |
|
|
143
|
+
| `scale` | 비례 확대 | 탱크 몸체 같은 단일 덩어리 |
|
|
144
|
+
| `fixed` | 크기는 그대로, 자리만 따라간다 | 모터 · 계기 · 노즐 |
|
|
145
|
+
| `stretch` | 한 축만 늘어난다 | 프레임 · 벨트 · 레일 |
|
|
146
|
+
| `repeat` | **개수가 늘어난다** — 새 geometry 가 생기지 않는다 | 롤러 · 선반 단 |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## `FigureBlueprint` — 청사진
|
|
151
|
+
|
|
152
|
+
`compile(source)` 가 낸다. 저장하지 않는다 — 정본에서 언제든 다시 만든다.
|
|
153
|
+
|
|
154
|
+
| 필드 | 타입 | 뜻 |
|
|
155
|
+
| -------------------------- | ----------------------- | ----------------------------------------- |
|
|
156
|
+
| `version` | `number` | `BLUEPRINT_VERSION`. 형식이 바뀌면 오른다 |
|
|
157
|
+
| `type` · `base` · `anchor` | | 정본에서 그대로 |
|
|
158
|
+
| `budget` | `{ triangles, groups }` | 예산. `groups` 가 **재는 값**이다 |
|
|
159
|
+
| `groups` | `BlueprintGroup[]` | 재질별로 병합된 묶음 |
|
|
160
|
+
| `points` | `BlueprintPoint[]` | 애니메이션 포인트. 부품 이름으로 가리킨다 |
|
|
161
|
+
| `labels` | `BlueprintLabel[]` | 라벨. `at` 이 부품 이름 |
|
|
162
|
+
| `violations` | `Violation[]` | 정책을 넘은 것. 저작 화면이 보인다 |
|
|
163
|
+
|
|
164
|
+
### `BlueprintGroup` — 병합 단위 = draw call 하나
|
|
165
|
+
|
|
166
|
+
| 필드 | 뜻 |
|
|
167
|
+
| ------------- | --------------------------------------------------- |
|
|
168
|
+
| `materialKey` | MaterialBank 키 |
|
|
169
|
+
| `material` | 정본의 `PartMaterial` 그대로 |
|
|
170
|
+
| `slot` | 재질 슬롯 |
|
|
171
|
+
| `animated` | 애니메이션이 걸린 묶음. **걸리면 부품 하나만 든다** |
|
|
172
|
+
| `members` | `BlueprintMember[]` |
|
|
173
|
+
|
|
174
|
+
### `BlueprintMember` — 묶음 안의 부품
|
|
175
|
+
|
|
176
|
+
| 필드 | 뜻 |
|
|
177
|
+
| ------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
|
178
|
+
| `name` · `primitive` · `transform` · `shape` · `sizing` · `repeat` | 정본에서 |
|
|
179
|
+
| `segments` | 곡면 분할 수. **2D 톱뷰를 3D 와 같은 각으로 근사하려면 필요하다** |
|
|
180
|
+
| `geometryKey` | GeometryBank 키. 실체가 아니라 참조다 |
|
|
181
|
+
| `triangles` | 이 부품의 삼각형 수 **추정** |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 키
|
|
186
|
+
|
|
187
|
+
| 함수 | 식 |
|
|
188
|
+
| --------------- | --------------------------------------------- |
|
|
189
|
+
| `geometryKeyOf` | `primitive\|WxHxD[\|s분할][\|r반경][\|p점수]` |
|
|
190
|
+
| `materialKeyOf` | `token\|preset\|f\|t` (`-` 는 없음) |
|
|
191
|
+
| `groupKeyOf` | `materialKey\|\|slot` |
|
|
192
|
+
|
|
193
|
+
**geometry 키에 치수가 든다.** 경로에서 오는 형상은 단위로 정규화하면 모서리 반경
|
|
194
|
+
비율이 망가지므로 치수를 굽고, 대신 같은 치수끼리만 공유한다. **자리는 키에 들지
|
|
195
|
+
않는다** — 어디 있든 같은 형상이다.
|
|
196
|
+
|
|
197
|
+
## 한도와 프리셋
|
|
198
|
+
|
|
199
|
+
| | 값 | 근거 |
|
|
200
|
+
| ----------------------------- | ---------------- | -------------------------------------------------------------------- |
|
|
201
|
+
| `SEGMENT_PRESETS` | 8 · 12 · 16 | 셋으로 제한하면 geometry 키가 셋으로 묶인다 |
|
|
202
|
+
| `LIMITS.minSegments` | 3 | 2 이하는 면이 생기지 않는다 |
|
|
203
|
+
| `LIMITS.materialGroups` | 3 | 자산당 draw call |
|
|
204
|
+
| `LIMITS.animationPoints` | 2 | 포인트마다 묶음이 늘고 프레임마다 갱신이 는다 |
|
|
205
|
+
| `LIMITS.transparentMaterials` | 1 | 투명은 별도 정렬 경로 |
|
|
206
|
+
| `LIMITS.surfaceTextures` | 1 | 무늬·그라디언트는 재질을 나눠 쓸 수 없다. 라벨 판 한 군데쯤은 봐준다 |
|
|
207
|
+
| `PART_LIMIT` | S 3 · M 8 · L 20 | 등급별 부품 수 |
|
|
208
|
+
|
|
209
|
+
**미감을 위한 제약이 그대로 성능의 근거다.** 격자 스냅 · 분할 프리셋 · 색 팔레트 ·
|
|
210
|
+
디테일 등급은 「감각적인 로우폴리」를 위해 둔 규칙인데, 같은 규칙이 geometry 키와
|
|
211
|
+
material 키를 줄인다.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 원가와 점수 — 저작자가 보는 지표
|
|
216
|
+
|
|
217
|
+
`costOf(blueprint, instances = 100)` 는 **인스턴스가 늘 때 무엇이 늘고 무엇이 그대로
|
|
218
|
+
인지**를 낸다. 그대로인 것이 곧 재활용되는 것이다.
|
|
219
|
+
|
|
220
|
+
| 필드 | 뜻 |
|
|
221
|
+
| -------------------------------- | ----------------------------------------------------- |
|
|
222
|
+
| `triangles` · `groups` | Figure 하나의 삼각형 수와 묶음 수 |
|
|
223
|
+
| `distinctShapes` | 서로 다른 형상 수. 부품 자리보다 적으면 그만큼 되쓴다 |
|
|
224
|
+
| `distinctMaterials` | 서로 다른 재질 수 |
|
|
225
|
+
| `shapeReuse` | `distinctShapes / parts`. 1 이면 되쓰기 없음 |
|
|
226
|
+
| `at.triangles` · `at.drawCalls` | **인스턴스에 비례해 오른다** |
|
|
227
|
+
| `at.geometries` · `at.materials` | **인스턴스와 무관하다** |
|
|
228
|
+
| `at.drawCallsIfInstanced` | `InstancedMesh` 를 쓰면 도달할 draw call |
|
|
229
|
+
|
|
230
|
+
`describeCost(cost)` 는 같은 값을 사람이 읽는 줄로 옮긴다. 값을 다시 계산하지 않는다.
|
|
231
|
+
|
|
232
|
+
### 점수 — 근거는 선언된 한도뿐이다
|
|
233
|
+
|
|
234
|
+
`scoreOf(blueprint)` 가 낸다. **새로 지어낸 기준이 하나도 없다** — 점수를 매기려고
|
|
235
|
+
만든 수가 섞이면 「왜 그 점수냐」에 답할 수 없기 때문이다.
|
|
236
|
+
|
|
237
|
+
| 한도 | 값 | 무엇을 지키나 |
|
|
238
|
+
| ------------- | ----------------------------- | ------------------------------------------------ |
|
|
239
|
+
| `groups` | `LIMITS.materialGroups` | 인스턴스 하나마다 치르는 draw call |
|
|
240
|
+
| `animations` | `LIMITS.animationPoints` | 묶음이 늘고 프레임마다 갱신이 는다 |
|
|
241
|
+
| `transparent` | `LIMITS.transparentMaterials` | 투명은 별도 정렬 경로 |
|
|
242
|
+
| `parts` | `PART_LIMIT[detailLevel]` | 등급이 정한 상한. 등급이 없으면 이 항목이 빠진다 |
|
|
243
|
+
|
|
244
|
+
- `score` — **가장 빠듯한 한도에 남은 여유**(0–100). 평균이 아니다. 넷 중 하나만 꽉
|
|
245
|
+
차 있어도 대량 배치에서는 그것이 병목이 되므로, 평균을 내면 병목이 여유에 묻힌다.
|
|
246
|
+
- `grade` — **A** 여유 절반 이상 · **B** 4분의 1 이상 · **C** 한도 안 · **D** 한도 초과.
|
|
247
|
+
D 는 곧 위반이 있다는 뜻이고 사유는 `blueprint.violations` 에 있다.
|
|
248
|
+
- `tightest` — 어느 한도가 제일 빠듯한가. **여기부터 고치면 된다.**
|
|
249
|
+
- `reuse` — 형상 되쓰기. **점수에 넣지 않는다.** 되쓰기는 많을수록 좋지만 한도가
|
|
250
|
+
없다. 점수에 섞으면 「형상을 억지로 같게 만들라」는 신호가 되어 저작을 왜곡한다.
|
|
251
|
+
|
|
252
|
+
### 추이
|
|
253
|
+
|
|
254
|
+
`costCurve(blueprint, { from, to })` 는 1 개에서 10,000 개까지(기본) 자릿수마다 1·2·5
|
|
255
|
+
간격으로 훑어 점들을 낸다. `costCurveSvg` 는 그것을 SVG 문자열로 낸다 — 의존을 만들지
|
|
256
|
+
않으려고 문자열이며, 축과 글자는 `currentColor` 라 밝은 테마·어두운 테마 양쪽에서
|
|
257
|
+
읽힌다.
|
|
258
|
+
|
|
259
|
+
두 축 모두 로그다. 선형으로 그리면 10,000 쪽 값이 화면을 다 먹어, 정작 보여 줄 것 —
|
|
260
|
+
**오르는 선과 평평한 선의 대비** — 이 안 보인다.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 구현 상태 — 선언됐으나 아직 동작하지 않는 것
|
|
265
|
+
|
|
266
|
+
**이 절이 이 문서에서 가장 중요하다.** 형식에 자리가 있다고 해서 그 자리가 동작하는
|
|
267
|
+
것은 아니다. 아래는 정본·청사진에 실려 나가지만 **읽어서 무언가 하는 코드가 아직
|
|
268
|
+
없는** 것들이다.
|
|
269
|
+
|
|
270
|
+
| 선언 | 검증 | 청사진에 실림 | 읽는 곳 |
|
|
271
|
+
| --------------------- | ---- | ------------- | ------------------------------------------------------- |
|
|
272
|
+
| `sizing` · `repeat` | ✓ | ✓ | **없음** — 네 규칙이 지금은 전부 같게 동작한다 |
|
|
273
|
+
| `anchor` | ✓ | ✓ | **없음** |
|
|
274
|
+
| `labels` | ✓ | ✓ | **없음** — 그리는 곳이 없다 |
|
|
275
|
+
| `transform.rotation` | ✓ | ✓ | **없음** — 렌더가 무시한다 |
|
|
276
|
+
| `styleKit` | ✓ | — | **없음** |
|
|
277
|
+
| `material.surface` | ✓ | ✓ | **없음** — 셈에는 들어가지만 그리는 곳이 아직 없다 |
|
|
278
|
+
| `points`(애니메이션) | ✓ | ✓ | 부품 이름 → mesh 배선까지. **실제로 움직이지는 않는다** |
|
|
279
|
+
| `FigureInstanceModel` | — | — | **없음** — 타입만 있다 |
|
|
280
|
+
|
|
281
|
+
`sizing` 을 예로 들면, `'repeat'` 는 「개수가 늘어난다」로 정의되어 있지만 늘어나지
|
|
282
|
+
않는다. `'fixed'` 도 크기가 고정되지 않는다. 지금은 **모든 부품이 `'scale'` 처럼**
|
|
283
|
+
인스턴스 크기에 비례한다.
|
|
284
|
+
|
|
285
|
+
이 표는 기능이 붙을 때마다 함께 고친다. 「선언은 있는데 길이 없다」를 문서가 감추면
|
|
286
|
+
읽는 사람이 없는 기능을 있다고 믿는다.
|
|
287
|
+
|
|
288
|
+
### 폴리곤 수
|
|
289
|
+
|
|
290
|
+
`trianglesOf(part)` 와 `blueprint.budget.triangles` 는 **식으로 낸 추정치**다.
|
|
291
|
+
실제 병합 geometry 의 삼각형 수와 맞는지는 **아직 확인되지 않았다** — 특히
|
|
292
|
+
`polygon` 의 `2(n−2) + 2n` 은 `ExtrudeGeometry` 의 실제 cap 삼각분할과 다를 수 있다.
|
|
293
|
+
|
|
294
|
+
Figure 한 종의 값과 인스턴스별 추이는 `costOf` · `costCurve` 로 낸다. **보드 전체
|
|
295
|
+
합계**(여러 타입이 섞인 판)는 내지 않는다 — 필요한 것이 Figure 단위라서 범위에서 뺐다.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# 검증 — 오류와 위반
|
|
2
|
+
|
|
3
|
+
`validate(source)` 는 두 벌을 돌려준다.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const { errors, violations } = validate(source)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
| | 뜻 | 컴파일 |
|
|
10
|
+
| ------------ | --------------------------------------------------- | ----------- |
|
|
11
|
+
| `errors` | 형식·정합성이 깨졌다. 무엇을 세워야 할지 알 수 없다 | **막는다** |
|
|
12
|
+
| `violations` | 형식은 맞다. 정책을 넘었을 뿐이다 | 막지 않는다 |
|
|
13
|
+
|
|
14
|
+
가르는 기준은 **「이 상태로 세울 수 있나」** 하나다. `depth` 가 없으면 세울 수 없다 —
|
|
15
|
+
오류다. 재질이 넷이면 세울 수는 있고 다만 draw call 이 는다 — 위반이다.
|
|
16
|
+
|
|
17
|
+
**정책은 형식이 성립할 때만 본다.** `errors` 가 하나라도 있으면 `violations` 는 비어
|
|
18
|
+
있다. 망가진 입력에 대고 「재질이 많다」고 말해 봐야 도움이 안 되기 때문이다.
|
|
19
|
+
|
|
20
|
+
## 코드는 목록으로 잠겨 있다
|
|
21
|
+
|
|
22
|
+
`ERROR_CODES` · `VIOLATION_CODES` 가 정본이고 `FigureError.code` · `Violation.code` 가
|
|
23
|
+
그 값으로 좁혀져 있다. **목록에 없는 코드는 타입 검사가 막는다.** 이 문서와 목록이
|
|
24
|
+
어긋나면 `src/docs.test.ts` 가 깨진다.
|
|
25
|
+
|
|
26
|
+
소비처는 코드로 분기한다. 문구(`message`)는 사람이 읽는 것이라 바뀔 수 있으니
|
|
27
|
+
코드에 의존한다.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 오류 (`errors`)
|
|
32
|
+
|
|
33
|
+
`{ code, path, message }` — `path` 는 어디서 났는지 가리킨다. 예: `parts[2].transform.width`.
|
|
34
|
+
|
|
35
|
+
### 형태
|
|
36
|
+
|
|
37
|
+
| 코드 | 언제 |
|
|
38
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `not-object` | 객체여야 할 자리에 객체가 아닌 것이 왔다 (정본 자체, 부품, `transform.rotation`, `material`, `animation`, `label`, `Vec3`) |
|
|
40
|
+
| `not-finite` | 수여야 할 자리가 수가 아니거나 `NaN`·`Infinity` 다 |
|
|
41
|
+
| `not-positive` | 0 보다 커야 하는데 아니다 (`base` 의 세 변) |
|
|
42
|
+
| `not-integer` | `segments` 가 정수가 아니다 |
|
|
43
|
+
| `not-boolean` | `material.flatShading` · `material.transparent` 가 참·거짓이 아니다 |
|
|
44
|
+
| `not-allowed` | 열거 값이 목록 밖이다 (`primitive` · `preset` · `slot` · `sizing` · `axis` · `detailLevel` · `animation.kind` · `label.when`) |
|
|
45
|
+
|
|
46
|
+
### 정본 뼈대
|
|
47
|
+
|
|
48
|
+
| 코드 | 언제 | 왜 |
|
|
49
|
+
| --------------- | ----------------------------------------- | --------------------------------------------------------- |
|
|
50
|
+
| `missing-type` | `type` 이 없거나 빈 문자다 | **저장되는 식별자다.** 보드가 이 이름으로 청사진을 찾는다 |
|
|
51
|
+
| `missing-base` | `base` 가 없다 | 배치 크기가 이 비율에서 나온다 |
|
|
52
|
+
| `missing-parts` | `parts` 가 없거나 비었다 | 부품이 하나는 있어야 한다. 여기서 검사를 멈춘다 |
|
|
53
|
+
| `bad-style-kit` | `styleKit` 이 빈 문자이거나 문자가 아니다 | |
|
|
54
|
+
|
|
55
|
+
### 부품
|
|
56
|
+
|
|
57
|
+
| 코드 | 언제 | 왜 |
|
|
58
|
+
| ------------------- | ---------------------------------------------- | ---------------------------------------------------------------------- |
|
|
59
|
+
| `missing-name` | `name` 이 없거나 빈 문자다 | **바인딩·애니메이션·슬롯이 이 이름을 가리킨다** |
|
|
60
|
+
| `duplicate-name` | 이름이 겹친다 | 겹치면 어느 부품을 가리키는지 정해지지 않는다 |
|
|
61
|
+
| `missing-transform` | `transform` 이 없다 | |
|
|
62
|
+
| `missing-depth` | `transform.depth` 가 없다 | **0 으로 갈음하지 않는다.** 두께를 모르는 것과 두께가 0 인 것은 다르다 |
|
|
63
|
+
| `too-few-segments` | `segments` 가 `LIMITS.minSegments`(3) 미만이다 | 2 이하는 면이 생기지 않는다 |
|
|
64
|
+
|
|
65
|
+
### 단면
|
|
66
|
+
|
|
67
|
+
| 코드 | 언제 |
|
|
68
|
+
| -------------------- | -------------------------------------------------------- |
|
|
69
|
+
| `missing-shape-path` | `polygon` 인데 `shape.path` 가 없거나 점이 3 개 미만이다 |
|
|
70
|
+
| `bad-shape-point` | `shape.path` 의 점이 `{ x, y }` 가 아니다 |
|
|
71
|
+
|
|
72
|
+
### 재질
|
|
73
|
+
|
|
74
|
+
| 코드 | 언제 | 왜 |
|
|
75
|
+
| ----------------------- | --------------------------- | -------------------------------------- |
|
|
76
|
+
| `missing-material` | `material` 이 없다 | |
|
|
77
|
+
| `material-unresolvable` | `token` 도 `preset` 도 없다 | 무슨 색·무슨 질감인지 정할 근거가 없다 |
|
|
78
|
+
|
|
79
|
+
### 크기 반응
|
|
80
|
+
|
|
81
|
+
| 코드 | 언제 |
|
|
82
|
+
| ---------------- | -------------------------------------------- |
|
|
83
|
+
| `missing-repeat` | `sizing` 이 `'repeat'` 인데 `repeat` 가 없다 |
|
|
84
|
+
|
|
85
|
+
### 애니메이션
|
|
86
|
+
|
|
87
|
+
| 코드 | 언제 | 왜 |
|
|
88
|
+
| --------------- | ------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `missing-axis` | 회전인데 `axis` 가 없다 | 어느 축으로 도는지 모른다 |
|
|
90
|
+
| `missing-pivot` | 회전인데 `pivot` 이 없다 | 없으면 부품 **자기 중심**으로 돈다. 교반 축이 몸체 가운데를 돌아야 하면 결과가 완전히 달라진다 |
|
|
91
|
+
|
|
92
|
+
### 예외 표면
|
|
93
|
+
|
|
94
|
+
| 코드 | 언제 |
|
|
95
|
+
| --------------------- | --------------------------------------------------- |
|
|
96
|
+
| `missing-surface-ref` | `material.surface.ref` 가 없다 — 무엇을 쓸지 모른다 |
|
|
97
|
+
|
|
98
|
+
### 라벨
|
|
99
|
+
|
|
100
|
+
| 코드 | 언제 |
|
|
101
|
+
| ---------------------- | --------------------------------------------- |
|
|
102
|
+
| `missing-label-name` | `label.name` 이 없다 |
|
|
103
|
+
| `missing-label-source` | `label.source` 가 없다 — 무엇을 보일지 모른다 |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 위반 (`violations`)
|
|
108
|
+
|
|
109
|
+
`{ code, message, actual?, limit? }` — 한도를 넘은 것은 `actual` 과 `limit` 을 함께
|
|
110
|
+
낸다. 저작 화면이 「8 개 중 3 개 한도」처럼 보일 수 있게 하려는 것이다.
|
|
111
|
+
|
|
112
|
+
### 쓰이지 않는 값
|
|
113
|
+
|
|
114
|
+
| 코드 | 언제 |
|
|
115
|
+
| --------------------- | ------------------------------------------------------------------------------------- |
|
|
116
|
+
| `segments-off-preset` | `segments` 가 프리셋(8 · 12 · 16) 밖이다. 값은 쓰이지만 geometry 키가 그만큼 늘어난다 |
|
|
117
|
+
| `segments-ignored` | 곡면이 없는 프리미티브에 `segments` 를 주었다 |
|
|
118
|
+
| `repeat-ignored` | `sizing` 이 `'repeat'` 가 아닌데 `repeat` 를 주었다 |
|
|
119
|
+
|
|
120
|
+
### 예산
|
|
121
|
+
|
|
122
|
+
| 코드 | 한도 | 왜 |
|
|
123
|
+
| --------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `detail-level-missing` | — | 등급이 없으면 부품 수 예산을 잡을 수 없다 |
|
|
125
|
+
| `too-many-parts` | `PART_LIMIT[detailLevel]` (S 3 · M 8 · L 20) | |
|
|
126
|
+
| `too-many-animation-points` | `LIMITS.animationPoints` (2) | 포인트마다 묶음이 하나 늘고, 프레임마다 갱신이 는다 |
|
|
127
|
+
| `too-many-material-groups` | `LIMITS.materialGroups` (3) | **자산당 draw call 이 는다.** 세는 단위는 `groupKeyOf` — 컴파일러가 합치는 단위와 같은 식이다 |
|
|
128
|
+
| `too-many-transparent` | `LIMITS.transparentMaterials` (1) | 투명 재질은 별도 정렬 경로를 탄다 |
|
|
129
|
+
| `too-many-always-labels` | 1 | `'always'` 라벨은 대량 배치에서 DOM·텍스처를 늘린다 |
|
|
130
|
+
| `too-many-surface-textures` | `LIMITS.surfaceTextures` (1) | 무늬·그라디언트는 **재질을 나눠 쓸 수 없다** — 인스턴스 수만큼 재질이 생긴다 |
|
|
131
|
+
|
|
132
|
+
### 묶음 수를 세는 법
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
묶음 수 = 재질·슬롯 조합 수 + 애니메이션 포인트 수
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
애니메이션이 걸린 부품은 **제 묶음을 따로 갖는다.** 합쳐 버리면 그 부품만 따로
|
|
139
|
+
움직일 수 없기 때문이다.
|
|
140
|
+
|
|
141
|
+
`groupKeyOf` 에 드는 것 — `token` · `preset` · `flatShading` · `transparent` · `slot`.
|
|
142
|
+
투명 여부와 flatShading 이 키에 드는 이유는 셰이더와 정렬 경로가 달라 실제로 합칠 수
|
|
143
|
+
없기 때문이다.
|
|
144
|
+
|
|
145
|
+
**검사와 컴파일이 같은 함수를 쓴다**(`src/keys.ts`). 다르면 「한도를 지켰다」는 말이
|
|
146
|
+
실제 묶음 수와 어긋나고, 그 어긋남은 오류 없이 성능으로만 나타난다.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 컴파일
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const blueprint = compile(source)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`errors` 가 있으면 `FigureCompileError` 를 던진다. 예외에 `errors` 목록이 담겨 있다.
|
|
157
|
+
|
|
158
|
+
`violations` 는 던지지 않고 **청사진에 실려 나간다**(`blueprint.violations`). 저작
|
|
159
|
+
화면이 그것을 보인다.
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hatiolab/figure-model",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Figure 저작 결과의 정본 형식과 검증 — 재질별로 병합되고 이름이 붙은 부품 그래프. 3D 로 저작한 형상 하나가 씬 컴포넌트로 서는 데 필요한 것을 담는다.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"figure",
|
|
7
|
+
"low-poly",
|
|
8
|
+
"3d",
|
|
9
|
+
"blueprint",
|
|
10
|
+
"authoring",
|
|
11
|
+
"things-scene",
|
|
12
|
+
"digital-twin",
|
|
13
|
+
"hatiolab"
|
|
14
|
+
],
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"author": "heartyoh@hatiolab.com",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/hatiolab/figure-model.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/hatiolab/figure-model/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/hatiolab/figure-model#readme",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./types": {
|
|
35
|
+
"types": "./dist/types.d.ts",
|
|
36
|
+
"default": "./dist/types.js"
|
|
37
|
+
},
|
|
38
|
+
"./validate": {
|
|
39
|
+
"types": "./dist/validate.d.ts",
|
|
40
|
+
"default": "./dist/validate.js"
|
|
41
|
+
},
|
|
42
|
+
"./blueprint": {
|
|
43
|
+
"types": "./dist/blueprint.d.ts",
|
|
44
|
+
"default": "./dist/blueprint.js"
|
|
45
|
+
},
|
|
46
|
+
"./cost": {
|
|
47
|
+
"types": "./dist/cost.d.ts",
|
|
48
|
+
"default": "./dist/cost.js"
|
|
49
|
+
},
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"docs",
|
|
55
|
+
"README.md",
|
|
56
|
+
"CHANGELOG.md"
|
|
57
|
+
],
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc -p tsconfig.build.json",
|
|
66
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
67
|
+
"test": "node --test src/*.test.ts",
|
|
68
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=100 --test-coverage-branches=95 --test-coverage-functions=100 src/*.test.ts",
|
|
69
|
+
"bench": "node src/perf.bench.ts",
|
|
70
|
+
"format": "prettier --write \"src/**/*.ts\" \"docs/**/*.md\" \"*.md\" \"*.json\"",
|
|
71
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"docs/**/*.md\" \"*.md\" \"*.json\"",
|
|
72
|
+
"clean": "rm -rf dist",
|
|
73
|
+
"prepublishOnly": "npm run clean && npm run check && npm test && npm run build"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "^26.4.1",
|
|
77
|
+
"prettier": "^3.9.6",
|
|
78
|
+
"typescript": "^5.6.0"
|
|
79
|
+
}
|
|
80
|
+
}
|