@hatiolab/figure-model 0.1.9 → 0.1.12

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/docs/format.md CHANGED
@@ -56,11 +56,12 @@ transform.rotation { x, y, z } 축별 회전(도), 선택
56
56
  | `transform` | `PartTransform` | ✓ | 자리와 크기 |
57
57
  | `material` | `PartMaterial` | ✓ | 색과 질감 |
58
58
  | `segments` | `number` | | 곡면 분할 수. `cylinder` · `sphere` 만 |
59
- | `shape` | `PartShape` | | 단면. `rect` · `polygon` 만 |
60
- | `slot` | `'cap' \| 'side'` | | 상태 색이 연동될 재질 슬롯. 없으면 상태를 표현하지 않는다 |
59
+ | `shape` | `PartShape` | | 단면. `rect` · `polygon` 만. `hollow` 로 **속을 판다** |
60
+ | `materialSlot` | `'cap' \| 'side'` | | 상태 색이 연동될 재질 슬롯. 없으면 상태를 표현하지 않는다. **`slot` 이 아니다** — 그 낱말은 보유물이 앉는 자리 쪽 임자가 있다 |
61
61
  | `sizing` | `SizingRule` | | 크기 반응 규칙. 기본 `'scale'` |
62
+ | `keepRound` | `boolean` | | 단면을 원으로 지킬까. 곡면만. 기본 `true` — **켜면 draw call 하나를 쓴다** |
63
+ | `anchor` | `PartAnchor` | | 축마다 **어느 면을 붙잡나**. `sizing` 이 정한 것을 덮어쓴다 |
62
64
  | `repeat` | `{ axis, pitch }` | | `sizing` 이 `'repeat'` 일 때 필수 |
63
- | `animation` | `AnimationPoint` | | 애니메이션 포인트 |
64
65
  | `label` | `LabelSpec` | | 텍스트. 아래 표 |
65
66
 
66
67
  ### `PrimitiveKind`
@@ -123,22 +124,85 @@ mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민
123
124
  한 Figure 에 `LIMITS.surfaceTextures`(1) 개까지 봐준다. 넘으면 위반이고
124
125
  원가표(`costOf`)의 재질 수와 점수(`scoreOf`)에 그대로 드러난다.
125
126
 
126
- ### `AnimationPoint`
127
+ ### 움직임 — `AnimationClip` · `AnimationChannel` · `Keyframe`
127
128
 
128
- | 필드 | 타입 | 뜻 |
129
- | ------- | ------------------- | ----------------------------------------------------------------------------------- |
130
- | `kind` | `AnimationKind` | `fade` · `heartbeat` · `moving` · `outline` · `rotation` · `vibration` · `waypoint` |
131
- | `axis` | `'x' \| 'y' \| 'z'` | 회전은 필수. **부품 기준**이다 — 눕힌 실린더는 제 축이 여전히 `y` 다 |
132
- | `pivot` | `Vec3` | **회전은 필수.** 부품 기준 로컬. 없으면 회전축이 부품 자기 중심이 된다 |
133
- | `turnsPerSecond` | `number` | 초당 회전 수. 0 이상. 안 적으면 런타임 기본값. **지금 도는지는 인스턴스가 정한다** |
129
+ **glTF 의 애니메이션 구조 그대로다.** 이 형식을 다른 3D 형식으로 내보낼 수 있어야 하고,
130
+ 그러려면 구조가 같아야 한다. 「종류」를 고르는 목록이 아니다 — 어느 부품의 어느 속성을
131
+ 시간에 따라 어떻게 움직이는지를 적는다.
134
132
 
135
- `axis` 와 `pivot` 은 둘 다 **부품 기준**이다. 저작자가 뜻하는 것은 「제 축으로 돈다」이기
136
- 때문이다 — 롤러는 눕히든 세우든 제 긴 축으로 돈다. Figure 기준으로 읽으면 부품을 돌릴
137
- 때마다 축도 같이 고쳐 적어야 한다.
133
+ ```
134
+ FigureSource.animations[] clip
135
+ └ channels[] 채널 — (부품, 경로) + 키프레임
136
+ ```
137
+
138
+ #### `AnimationClip`
139
+
140
+ | 필드 | 타입 | 뜻 |
141
+ | ---------- | --------------------- | ------------------------------------------------------------ |
142
+ | `name` | `string` | **저장되는 식별자.** 인스턴스가 이 이름으로 구동한다 |
143
+ | `drive` | `'loop' \| 'hold'` | 안 적으면 `loop` |
144
+ | `channels` | `AnimationChannel[]` | 하나는 있어야 한다 |
145
+
146
+ 길이는 적지 않는다 — **마지막 키의 시각이 곧 길이다.** 따로 적으면 어긋날 수 있고,
147
+ 어긋나면 어느 쪽이 맞는지 알 방법이 없다.
148
+
149
+ #### `AnimationChannel`
150
+
151
+ | 필드 | 타입 | 뜻 |
152
+ | --------------- | ---------------------------------------- | ---------------------------------------------------------- |
153
+ | `target` | `string` | 어느 부품인가. `FigurePart.name` |
154
+ | `path` | `'translation' \| 'rotation' \| 'scale'` | 무엇을 움직이나 |
155
+ | `pivot` | `Vec3` | 회전 중심. `rotation` 에서만. 안 적으면 부품 제 중심 |
156
+ | `interpolation` | `'linear' \| 'step'` | 안 적으면 `linear` |
157
+ | `keys` | `Keyframe[]` | **둘 이상.** 하나는 움직임이 아니라 자세다 |
158
+
159
+ #### `Keyframe`
160
+
161
+ | 필드 | 타입 | 뜻 |
162
+ | ------- | -------- | ------------------------------------------------------ |
163
+ | `at` | `number` | 시각(초). 0 이상, **오름차순** |
164
+ | `value` | `Vec3` | 경로마다 뜻이 다르다 — 아래 |
165
+
166
+ ```
167
+ translation 부품 기준 옮김
168
+ rotation 오일러 도(°) transform.rotation 과 같은 약속
169
+ scale 배율. 1 이 제 크기
170
+ ```
171
+
172
+ #### glTF 와 다른 자리 다섯
173
+
174
+ | 여기 | glTF | 왜 |
175
+ | --- | --- | --- |
176
+ | `target` 이 부품 **이름** | 노드 인덱스 | 이 형식은 처음부터 이름으로 가리킨다 |
177
+ | 회전 키가 **오일러 도** | 쿼터니언 | 손으로 적는 형식이다. `transform.rotation` 과 같은 약속 |
178
+ | `pivot` 이 채널에 | 부모 노드를 끼운다 | 부품이 겹치지 않아 끼울 노드가 없다 |
179
+ | `weights` 없음 | 있음 | 모프 타깃이 없다 |
180
+ | `drive` 를 형식이 적는다 | 재생은 런타임 | 트윈은 타임라인을 재생하지 않는다 |
181
+ | 채널·샘플러를 합침 | 나뉨 | 나눔은 정점 데이터를 아끼는 장치다. 손으로 적을 때는 한 겹일 뿐 |
182
+
183
+ **내보낼 때 주의**: 회전을 쿼터니언으로 바꾸면 **360° 한 바퀴가 항등이 되어** 키 둘로는
184
+ 표현이 안 된다. 180° 넘는 구간은 내보내는 쪽이 쪼개야 한다 — 표준 익스포터가 하는 일이다.
138
185
 
139
- 속도는 **저작한 값**이고, 램프와 같은 방식으로 인스턴스가 덮어쓴다. 형식이 `on` 을 적고
140
- `state.lamps` 가 지금을 정하듯, 형식이 `turnsPerSecond` 를 적고 `state.speeds` 가 지금을
141
- 정한다. `0` 은 멈춤이다.
186
+ #### `drive` — 트윈이 타임라인을 재생하지 않기 때문에 있다
187
+
188
+ clip 은 `t → 변환` 함수다. `t` 를 시계가 아니라 **상태**에서 받으면 트윈이 원하는 것이
189
+ 그대로 된다.
190
+
191
+ ```
192
+ loop 시각이 흐른다. 인스턴스가 주는 값은 배속 도는 롤러
193
+ hold 시각이 멈춰 있다. 인스턴스가 주는 값은 0~1 위치 열린 정도
194
+ ```
195
+
196
+ 그래서 「진동」·「맥동」은 별도 종류가 아니라 **왕복하는 키프레임**이다.
197
+
198
+ ```js
199
+ // 15° 씩 흔들린다
200
+ { name: 'shake', channels: [{ target: 'deck', path: 'rotation', keys: [
201
+ { at: 0, value: { x: 0, y: 0, z: -15 } },
202
+ { at: 0.25, value: { x: 0, y: 0, z: 15 } },
203
+ { at: 0.5, value: { x: 0, y: 0, z: -15 } }
204
+ ]}]}
205
+ ```
142
206
 
143
207
  ### `LabelSpec`
144
208
 
@@ -158,10 +222,111 @@ mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민
158
222
  | 값 | 뜻 | 예 |
159
223
  | --------- | -------------------------------------------------- | -------------------------- |
160
224
  | `scale` | 비례 확대 | 탱크 몸체 같은 단일 덩어리 |
161
- | `fixed` | 크기는 그대로, 자리만 따라간다 | 모터 · 계기 · 노즐 |
162
- | `stretch` | 한 축만 늘어난다 | 프레임 · 벨트 · 레일 |
225
+ | `fixed` | 크기는 그대로. **붙잡은 면에서 잰 거리**를 지킨다 | 모터 · 계기 · 노즐 |
226
+ | `stretch` | 가장 긴 축이 **양쪽 사이를 채운다** | 프레임 · 벨트 · 레일 |
163
227
  | `repeat` | **개수가 늘어난다** — 새 geometry 가 생기지 않는다 | 롤러 · 선반 단 |
164
228
 
229
+ ### `PartShape.hollow` — 속을 판다
230
+
231
+ 트레이 · 토트 · 통 · 케이스 · 프레임이 전부 이 모양이다. 없을 때는 바닥판 하나에 벽 넷을
232
+ 세워 만들었는데, 부품이 다섯인 것보다 나쁜 것은 **모양이 안 맞는다**는 것이었다 —
233
+ 셀트레이의 바닥판은 모서리를 24 로 깎았고 벽 넷은 저마다 12 로 깎은 판이라, 네 귀퉁이에서
234
+ 둥근 것 둘이 어긋나게 만났다.
235
+
236
+ | 필드 | 뜻 |
237
+ | ------- | ---------------------------------------------------------- |
238
+ | `wall` | 벽 두께. 안쪽 테는 바깥 테에서 이만큼 들어온다 |
239
+ | `floor` | 바닥 두께. 없으면 벽과 같다. **`0` 이면 위아래가 다 뚫린다** |
240
+
241
+ `size` 는 **바깥 치수**다. 프리미티브를 새로 만들지 않은 것은 「속을 판다」가 상자만의
242
+ 성질이 아니기 때문이다 — `polygon` 을 파면 U 자 가드 · ㄷ 자 프레임이 같은 규칙으로
243
+ 나온다. 다만 **오목한 단면은 아직 못 판다**(`hollow-not-convex`).
244
+
245
+ 삼각형은 이렇게 든다. 재 본 값이다.
246
+
247
+ ```
248
+ rect 각짐 12 cube 와 같다 — 민다고 비싸지지 않는다
249
+ rect round=24 140 모서리마다 8 점으로 뽑으므로 테가 36 점이 된다
250
+ 속 판 rect round=24 160 구멍은 +20. 고리의 캡이 통짜 캡보다 싸다
251
+ ```
252
+
253
+ **비싼 것은 미는 것이 아니라 라운드다.**
254
+
255
+ ### `keepRound` — 원통을 원통으로 둘까
256
+
257
+ 인스턴스가 축마다 다른 배율을 받으면 원기둥은 타원 기둥이 된다. AGV 를 길게 늘이면
258
+ 바퀴가 찌그러지고, 크레들을 높이면 롤이 납작해진다. 기본은 **지킨다**이다 — 원기둥이라고
259
+ 적어 놓고 원기둥이 아닌 것을 그리지 않는다.
260
+
261
+ 지키는 방식은 **단면의 두 축이 같은 배율을 받는 것**이고, 그 배율은 두 배율의
262
+ **기하평균**이다. 크기를 얼리는 것이 아니다 — 인스턴스가 커지면 원통도 커지되 원을
263
+ 지킨다. 작은 쪽에 맞추면 한 축만 늘인 인스턴스에서 지름이 묶여 **형태가 아니라 크기**를
264
+ 지키게 된다.
265
+
266
+ #### 비용
267
+
268
+ 합친 형상은 변환을 하나만 받는다. 원을 지키려면 제 변환이 있어야 하므로 **지키는 곡면
269
+ 부품 하나가 draw call 하나**다.
270
+
271
+ ```
272
+ 표본 열넷의 draw call 139 → 168 (+21%)
273
+ AGV 3 → 12 바퀴 · 기둥 · 램프가 다 원통이다
274
+ 작업자 4 → 13 팔다리가 원통이다
275
+ 전극 롤 크래들 8 → 17
276
+ ```
277
+
278
+ **형상은 계속 나눠 쓴다.** 따로 서는 것과 정점 버퍼를 나눠 쓰는 것은 다른 이야기다 —
279
+ 같은 규격 롤러 여덟은 묶음 여덟이어도 geometry 는 하나다. 그래서 이 비용은 인스턴싱이
280
+ 가장 잘 먹는 모양이기도 하다.
281
+
282
+ #### 스타일 밸런스
283
+
284
+ 끌 만한 것과 아닌 것이 갈린다.
285
+
286
+ ```
287
+ 켜 둔다 화면의 주역 롤 · 바퀴 · 탱크 · 기둥 · 파이프
288
+ 꺼도 된다 찌그러져도 모를 것 볼트 머리 · 작은 스터드 · 발 받침
289
+ ```
290
+
291
+ 로우폴리에서 지름 100 아래의 원통은 8 분할이면 이미 팔각형이라, 조금 찌그러져도 읽는
292
+ 사람이 눈치채지 못한다. 그런 것을 끄면 그 묶음이 다시 합쳐진다.
293
+
294
+ ### `PartAnchor` — 어느 면을 붙잡나
295
+
296
+ `sizing` 은 부품의 **크기**가 인스턴스를 어떻게 따라가는지만 말했다. **자리**는 언제나
297
+ 비례였고, 그래서 시그널 타워를 3 배로 세우면 기둥은 늘어나는데 램프들이 기둥 위 공중으로
298
+ 흩어졌다. 램프는 상자 안 어디쯤이 아니라 **기둥 꼭대기**에 붙어 있다.
299
+
300
+ | 값 | 뜻 |
301
+ | -------- | ------------------------------------------------------ |
302
+ | `scale` | **아무 면도 안 붙잡는다.** 상자와 함께 비례로 커진다 |
303
+ | `min` | 아래(왼쪽 · 뒤) 면에서 잰 거리를 지킨다 |
304
+ | `center` | 자리는 비례로 따라가고 **크기는 지킨다** |
305
+ | `max` | 위(오른쪽 · 앞) 면에서 잰 거리를 지킨다 |
306
+ | `span` | **양쪽을 다 붙잡는다.** 가운데가 늘어나 사이를 채운다 |
307
+
308
+ `repeat` 의 축에서는 **줄이 어떻게 되는가**를 말한다. 같은 「반복」이 두 가지다 —
309
+ 컨베이어의 롤러는 길어지면 개수가 늘고(`center`), 셀트레이의 셀은 커지면 셀이 커진다
310
+ (`scale`). 개수는 그 트레이가 몇 셀짜리냐이지 화면에 얼마나 크게 그렸느냐가 아니다.
311
+
312
+ 붙잡는 면의 **수**로 읽으면 하나의 규칙이다 — 0 개면 상자와 함께 커지고(`sizing: 'scale'`),
313
+ 1 개면 그 면에서 잰 거리를 지키고, 2 개면 사이를 채운다.
314
+
315
+ **적지 않으면 `sizing` 이 정한다.** `'fixed'` 는 가까운 면(부품의 면에서 상자의 면까지
316
+ 재서 짧은 쪽), `'stretch'` 는 가장 긴 축이 `span` 이고 나머지는 가까운 면, `'repeat'` 는
317
+ 늘어나는 축이 `center` 이고 나머지는 가까운 면이다. 표본 열넷 중 어느 것도 이 값을 적지
318
+ 않고 제대로 서므로, 기본값이 사람이 이미 뜻하는 것이라고 볼 만하다.
319
+
320
+ **`sizing` 하나로는 못 하는 말이 있어서 축별로 둔다.** 컨베이어의 데크가 그렇다 —
321
+ 길이(x)와 폭(z)은 상자를 채우고 두께(y)는 24 mm 그대로여야 한다.
322
+
323
+ ```
324
+ sizing: 'fixed', anchor: { x: 'span', y: 'max', z: 'span' }
325
+ ```
326
+
327
+ `sizing` 이 `'scale'` 인 부품에 적으면 `anchor-ignored` 다. 그 부품은 상자와 함께
328
+ 커지므로 붙잡을 면이 없다.
329
+
165
330
  ---
166
331
 
167
332
  ## `FigureBlueprint` — 청사진
@@ -174,7 +339,7 @@ mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민
174
339
  | `type` · `base` · `anchor` | | 원본 데이터에서 그대로 |
175
340
  | `budget` | `{ triangles, groups }` | 예산. `groups` 가 **재는 값**이다 |
176
341
  | `groups` | `BlueprintGroup[]` | 재질별로 병합된 묶음 |
177
- | `points` | `BlueprintPoint[]` | 애니메이션 포인트. 부품 이름으로 가리킨다 |
342
+ | `animations` | `BlueprintClip[]` | 움직임. 원본에 적은 것 그대로 |
178
343
  | `labels` | `BlueprintLabel[]` | 라벨. `at` 이 부품 이름 |
179
344
  | `violations` | `Violation[]` | 정책을 넘은 것. 저작 화면이 보인다 |
180
345
 
@@ -184,8 +349,8 @@ mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민
184
349
  | ------------- | --------------------------------------------------- |
185
350
  | `materialKey` | MaterialBank 키 |
186
351
  | `material` | 원본 데이터의 `PartMaterial` 그대로 |
187
- | `slot` | 재질 슬롯 |
188
- | `animated` | 애니메이션이 연결된 그룹인지. **연결되면 부품을 하나만 갖는다** |
352
+ | `materialSlot` | 재질 슬롯 |
353
+ | `animated` | clip 이 움직이는 부품이 든 그룹인지. **그러면 부품을 하나만 갖는다** |
189
354
  | `members` | `BlueprintMember[]` |
190
355
 
191
356
  ### `BlueprintMember` — 묶음 안의 부품
@@ -205,7 +370,7 @@ mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민
205
370
  | --------------- | --------------------------------------------- |
206
371
  | `geometryKeyOf` | `primitive\|WxHxD[\|s분할][\|r반경][\|p점수]` |
207
372
  | `materialKeyOf` | `token\|preset\|f\|t` (`-` 는 없음) |
208
- | `groupKeyOf` | `materialKey\|\|slot` |
373
+ | `groupKeyOf` | `materialKey\|\|materialSlot` |
209
374
 
210
375
  **geometry 키에 치수가 들어간다.** 경로에서 오는 형상은 단위로 정규화하면 모서리 반경
211
376
  비율이 망가지므로 치수를 굽고, 대신 같은 치수끼리만 공유한다. **자리는 키에 들지
@@ -218,7 +383,7 @@ mesh 계열은 치수만으로 선다. extrude 계열은 2D 단면을 위로 민
218
383
  | `SEGMENT_PRESETS` | 8 · 12 · 16 · 24 · 32 · 48 | 좁게 두면 단위 곡면이 그만큼만 창고에 남아 전체 자산이 나눠 쓴다 |
219
384
  | `LIMITS.minSegments` | 3 | 2 이하는 면이 생기지 않는다 |
220
385
  | `LIMITS.materialGroups` | 3 | 자산당 draw call |
221
- | `LIMITS.animationPoints` | 2 | 포인트마다 묶음이 늘고 프레임마다 갱신이 는다 |
386
+ | `LIMITS.animatedParts` | 2 | 움직이는 부품마다 묶음이 늘고 프레임마다 갱신이 는다 |
222
387
  | `LIMITS.transparentMaterials` | 1 | 투명은 별도 정렬 경로 |
223
388
  | `LIMITS.surfaceTextures` | 1 | 무늬·그라디언트는 재질을 나눠 쓸 수 없다. 라벨 판 한 군데쯤은 봐준다 |
224
389
  | `PART_LIMIT` | S 3 · M 8 · L 20 | 등급별 부품 수 |
@@ -254,7 +419,7 @@ material 키를 줄인다.
254
419
  | 한도 | 값 | 무엇을 지키나 |
255
420
  | ------------- | ----------------------------- | ------------------------------------------------ |
256
421
  | `groups` | `LIMITS.materialGroups` | 인스턴스 하나마다 치르는 draw call |
257
- | `animations` | `LIMITS.animationPoints` | 묶음이 늘고 프레임마다 갱신이 는다 |
422
+ | `animations` | `LIMITS.animatedParts` | 움직이는 부품마다 묶음이 늘고 프레임마다 갱신이 는다 |
258
423
  | `transparent` | `LIMITS.transparentMaterials` | 투명은 별도 정렬 경로 |
259
424
  | `parts` | `PART_LIMIT[detailLevel]` | 등급이 정한 상한. 등급이 없으면 이 항목이 빠진다 |
260
425
 
@@ -292,7 +457,7 @@ material 키를 줄인다.
292
457
  | `transform.rotation` | ✓ | ✓ | **없음** — 렌더가 무시한다 |
293
458
  | `styleKit` | ✓ | — | **없음** |
294
459
  | `material.surface` | ✓ | ✓ | **없음** — 셈에는 들어가지만 그리는 곳이 아직 없다 |
295
- | `points`(애니메이션) | ✓ | ✓ | 부품 이름 → mesh 배선까지. **실제로 움직이지는 않는다** |
460
+ | `animations` | ✓ | ✓ | 청사진까지. **런타임은 아직 clip 을 안 돌린다** |
296
461
  | `FigureInstanceModel` | — | — | **없음** — 타입만 있다 |
297
462
 
298
463
  `sizing` 을 예로 들면, `'repeat'` 는 「개수가 늘어난다」로 정의되어 있지만 늘어나지
package/docs/sizing.md ADDED
@@ -0,0 +1,183 @@
1
+ # 크기 반응 — 인스턴스가 커질 때 부품이 어떻게 되나
2
+
3
+ 저작한 형상 하나가 보드에 여러 크기로 놓인다. 컨베이어를 2400 으로도, 7200 으로도
4
+ 놓는다. 그때 부품마다 다르게 반응해야 한다 — 프레임은 길어지고, 모터는 그대로이고,
5
+ 롤러는 개수가 늘고, 다리는 늘어나 프레임을 떠받친다.
6
+
7
+ 이 문서는 그 반응을 **빠짐없이** 적는다. 낱개 규칙을 모아 놓은 것이 아니라, 「인스턴스가
8
+ 축마다 다른 배율을 받을 때 부품 하나에 일어날 수 있는 일」의 전부다.
9
+
10
+ ## 물음은 축마다 둘이다
11
+
12
+ 인스턴스 배율은 축마다 다르다 — `sx` · `sy` · `sz`. 부품 하나에 대해 축마다 물을 것이
13
+ 둘 있다.
14
+
15
+ ```
16
+ 크기 이 축의 길이가 어떻게 되나
17
+ 자리 이 축의 중심이 어디로 가나
18
+ ```
19
+
20
+ 세 축 × 둘 = 여섯 가지 답이 부품 하나의 반응을 완전히 정한다. **`anchor` 의 한 값이 그
21
+ 축의 둘을 함께 정한다** — 갈라 놓으면 「크기는 지키고 자리는 비례」처럼 성립하지 않는
22
+ 조합이 생긴다. 실제로 그것 때문에 크레들의 V 패드 둘이 ±80 에서 ±240 으로 벌어졌다.
23
+
24
+ ## 값 여섯 개가 전부다
25
+
26
+ | `anchor` | 크기 | 자리 | 붙잡는 자리 |
27
+ | ---------- | ------------- | --------------------------- | ----------- |
28
+ | `scale` | `× s` | `× s` | 없음 |
29
+ | `min` | 그대로 | 아래 면에서 잰 거리를 지킨다 | 한 곳 |
30
+ | `center` | 그대로 | 가운데에서 잰 거리를 지킨다 | 한 곳 |
31
+ | `max` | 그대로 | 위 면에서 잰 거리를 지킨다 | 한 곳 |
32
+ | `span` | 남는 만큼 채움 | 양쪽에서 잰 거리를 지킨다 | 두 곳 |
33
+ | `aspect-x` `aspect-y` `aspect-z` | 가리킨 축의 배율 | 가리킨 축의 배율 | 다른 축 |
34
+
35
+ 「아래·위」는 축의 −·+ 쪽이다. x 는 왼쪽·오른쪽, y 는 밑·위, z 는 뒤·앞이다.
36
+
37
+ ### 이 여섯이 왜 전부인가
38
+
39
+ 크기가 될 수 있는 것은 넷뿐이다 — 배율을 그대로 받거나(`scale`), 지키거나(`min` ·
40
+ `center` · `max`), 남는 만큼 채우거나(`span`), 다른 축을 따라가거나(`aspect-`). 자리는
41
+ 기준면이 어디냐로 갈리고 기준면은 셋뿐이다 — 아래 · 가운데 · 위.
42
+
43
+ 빠진 조합 둘을 굳이 확인해 두면 이렇다.
44
+
45
+ - **크기는 배율, 자리는 붙잡기.** `scale` 이 이미 그것이다. 바닥에 붙여 놓은 부품은
46
+ `scale` 로 두면 바닥에 남는다 — 상자 바닥이 `−h·s` 로 가고 부품 밑면도 그리로 간다.
47
+ - **크기는 그대로, 자리는 비례.** 한동안 `center` 가 이것이었고 **틀렸다.** 가운데
48
+ 가까이 저작한 것이 상자가 커질 때마다 밖으로 밀려 이웃과 갈라진다.
49
+
50
+ ## `sizing` 은 축별 기본값을 채우는 짧은 표기다
51
+
52
+ 부품마다 세 축을 다 적게 하면 저작이 못 견딘다. `sizing` 한 낱말이 세 축을 채우고,
53
+ `anchor` 가 축마다 그것을 덮는다.
54
+
55
+ | `sizing` | x · y · z 에 채워지는 것 |
56
+ | --------- | --------------------------------------------------------- |
57
+ | `scale` | 세 축 다 `scale` |
58
+ | `fixed` | 세 축 다 **가까운 면** |
59
+ | `stretch` | 가장 긴 축은 `span`, 나머지는 가까운 면 |
60
+ | `repeat` | 늘어나는 축은 `center`, 나머지는 가까운 면 |
61
+
62
+ **가까운 면**은 부품의 면에서 상자의 면까지 재서 짧은 쪽이다. 중심에서 재지 않는다 —
63
+ 중심으로 재면 굵은 부품과 얇은 부품이 같은 자리에서 다른 답을 낸다. 양쪽이 같으면
64
+ `center` 다.
65
+
66
+ 표본 열넷 중 대부분이 이 기본값만으로 제대로 선다. 그것이 이 기본값이 사람이 이미
67
+ 뜻하는 것이라는 증거다.
68
+
69
+ ## 축을 넘는 규칙 둘
70
+
71
+ 축마다 따로 정하는 것으로 안 되는 것이 둘 있다. 둘 다 **여러 축이 한 배율을 나눠
72
+ 받아야** 하는 경우다.
73
+
74
+ ### `keepRound` — 곡면의 단면은 원이다
75
+
76
+ `cylinder` · `sphere` 는 단면이 원이다. 축마다 다른 배율을 받으면 타원이 된다 —
77
+ 원기둥이라고 적어 놓고 원기둥이 아닌 것을 그리는 것이다.
78
+
79
+ 그래서 단면의 두 축(구는 세 축)이 **기하평균**을 함께 받는다. 작은 쪽에 맞추면 한 축만
80
+ 늘인 인스턴스에서 지름이 묶여, 형태가 아니라 **크기**를 지키게 된다.
81
+
82
+ 기본이 켜짐이고 값을 치른다 — 합친 형상은 변환을 하나만 받으므로 원을 지키는 곡면 부품
83
+ 하나가 draw call 하나다. 표본 열넷에서 139 가 168 이 되었다. `keepRound: false` 로 끄면
84
+ 다시 합쳐진다.
85
+
86
+ ```
87
+ 켜 둔다 화면의 주역 롤 · 바퀴 · 탱크 · 기둥 · 파이프
88
+ 끈다 기계가 안 깎은 것 사람의 팔다리 · 머리
89
+ 끈다 찌그러져도 모를 것 볼트 머리 · 작은 스터드
90
+ ```
91
+
92
+ 사람에게는 기계가 깎은 원통이 없다. 작업자를 전부 끄면 16 부품이 13 call 에서 4 call 이
93
+ 되는데, 그 4 는 쓰는 재질 수이므로 더 내려갈 자리가 없다는 뜻이다.
94
+
95
+ ### `aspect-` — 비율을 지킨다
96
+
97
+ 두 부품이 같은 것을 반대 방향에서 요구했다.
98
+
99
+ ```
100
+ 드라이룸 출입구 높이가 비례하고 **폭이 높이를 따라간다**
101
+ 문은 방이 넓어졌다고 넓어지지 않는다. 높아져서 넓어진다
102
+
103
+ 전극 롤 크래들 지름이 **z 를 따라간다**
104
+ z 가 기계의 깊이 900 이고 롤이 그 중 700 이다. 그 깊이가 지름의 자리다
105
+ 길이는 따로다 — x 가 웹 폭이다
106
+ ```
107
+
108
+ 축마다 제 배율을 받는 것으로는 둘 다 못 말한다. `aspect-y` 는 「이 축은 y 가 **끝낸**
109
+ 배율을 받는다」이고, 끝낸 배율은 그 축의 규칙이 정한다 — `scale` 이면 인스턴스 배율,
110
+ 붙잡은 축이면 1, `span` 이면 채운 만큼이다.
111
+
112
+ 자기 자신이나 또 다른 `aspect-` 를 가리킬 수는 없다(`bad-aspect-axis`). 따라갈 배율이
113
+ 정해지지 않는다.
114
+
115
+ **상자로 자르지 않는다.** 자르면 크레들 롤이 막힌다 — 지름은 기준 상자의 높이를 넘어서
116
+ 자란다. 기준 상자는 배치 비율의 분모이고 담장이 아니다(컨베이어의 발판이 이미 상자보다
117
+ 넓다). 값은 이렇다: 이미 상자를 꽉 채운 부품은 한 축만 커진 상자에서 비율을 지킬 수 없다.
118
+ 그런 부품은 여유를 두고 저작하거나 `span` 으로 채운다.
119
+
120
+ ## `repeat` — 줄이 늘어나나 커지나
121
+
122
+ 같은 「반복」이 두 가지다. 그 갈림을 늘어나는 축의 `anchor` 가 말한다.
123
+
124
+ ```
125
+ center 줄이 상자를 채운다 컨베이어 롤러 — 길어지면 개수가 늘고 한 칸은 그대로다
126
+ scale 줄이 상자와 함께 큰다 셀트레이 셀 — 커지면 셀이 커지고 개수는 그대로다
127
+ ```
128
+
129
+ 셀 개수는 그 트레이가 몇 셀짜리냐이지 화면에 얼마나 크게 그렸느냐가 아니다.
130
+
131
+ ## 저작할 때 묻는 순서
132
+
133
+ ```
134
+ 1 이 부품은 상자와 함께 커지나
135
+ 그렇다 → sizing: 'scale' · 끝
136
+ 2 어느 축이 상자를 채우나
137
+ 길이 방향 하나 → sizing: 'stretch'
138
+ 둘 이상 → sizing: 'fixed' + anchor 에 'span'
139
+ 3 채우지 않는 축은 무엇을 붙잡나
140
+ 적지 않으면 가까운 면. 가까운 면이 틀리면 min · center · max 로 적는다
141
+ 4 다른 부품과 한 뭉치인가
142
+ **같은 면을 붙잡게 한다.** 서로 다른 면을 잡으면 배율마다 사이가 벌어진다
143
+ 5 비율을 지켜야 하나
144
+ aspect- 로 기준 축을 가리킨다
145
+ 6 곡면인데 찌그러져도 되나
146
+ 되면 keepRound: false — draw call 하나를 아낀다
147
+ ```
148
+
149
+ 4 번이 가장 자주 틀린다. 롤프레스의 두 롤이 `min` 과 `max` 로 갈려 3 배에서 5200
150
+ 벌어졌고, 시그널 타워의 램프가 기둥과 다른 면을 잡아 공중으로 갔다.
151
+
152
+ ## 못 하는 말 하나 — 부품에 부모가 없다
153
+
154
+ 「선반에 달린 램프」 · 「하우징에 붙은 HMI」를 형식이 직접 말하지 못한다. 붙잡는 것은
155
+ 언제나 **기준 상자의 면**이고 다른 부품이 아니다.
156
+
157
+ 지금은 둘이 **같은 면을 붙잡게** 해서 우회한다. 상자의 같은 면에서 잰 거리를 둘 다
158
+ 지키면 서로의 거리도 지켜지기 때문이다. 그래서 롤프레스의 하우징이 `span` 으로 상자
159
+ 면까지 닿게 만들어 놓았다 — HMI 가 잡을 면과 하우징이 잡을 면을 같게 하려고.
160
+
161
+ 우회가 안 되는 자리도 있었다. 충방전기의 채널 램프는 선반과 같은 피치로 반복되는데, 두
162
+ 줄이 저마다 상자를 채우므로 줄 사이의 45 가 배율만큼 벌어졌다. 램프를 선반 앞턱에 물려
163
+ 같은 줄 중심을 쓰게 해서 넘겼다.
164
+
165
+ 부모가 생기면 이 우회들이 없어진다.
166
+
167
+ ## 무엇으로 확인하나
168
+
169
+ ```bash
170
+ node test/browser/check-figure-scaling.mjs # things-factory
171
+ ```
172
+
173
+ 보는 것 셋이다.
174
+
175
+ - **한 덩어리로 남나.** 닿는 부품을 이어 덩어리를 만들고, 1 배에서 한 덩어리이던 것이
176
+ 늘려도 한 덩어리인지 본다. 쌍으로 세면 안 된다 — AGV 바퀴가 차체 바닥과 26 벌어졌다고
177
+ 네 건이 났는데 그 사이는 underbody 가 메우고 있었다.
178
+ - **일곱 조합 전부.** x · y · z · x-y · x-z · y-z · x-y-z. 세 축을 같이 늘린 것만 보면
179
+ 축마다 따로 걸리는 규칙을 못 본다. 오늘 잡은 결함 둘이 다 한 축만 늘렸을 때 드러났다.
180
+ - **z-fighting.** 같은 평면 · **같은 법선** · 겹치는 발자국 셋이 다 맞을 때만이다. 맞대어
181
+ 놓은 것은 서로 등을 져서 뒷면이 잘려 나가므로 멀쩡하다.
182
+
183
+ 미리보기도 여덟을 나란히 놓는다 — 저작한 크기와 일곱 조합이다.
@@ -69,6 +69,9 @@ const { errors, violations } = validate(source)
69
69
  | 코드 | 언제 |
70
70
  | -------------------- | -------------------------------------------------------- |
71
71
  | `missing-shape-path` | `polygon` 인데 `shape.path` 가 없거나 점이 3 개 미만이다 |
72
+ | `hollow-too-thick` | 벽이나 바닥이 치수를 다 먹는다 — 팔 안쪽이 남지 않는다 |
73
+ | `hollow-not-convex` | 오목한 `polygon` 은 아직 못 판다 — 안쪽 테가 제 몸을 가로지른다 |
74
+ | `bad-aspect-axis` | `aspect-` 가 자기 자신이나 또 다른 `aspect-` 축을 가리킨다 — 따라갈 배율이 정해지지 않는다 |
72
75
  | `bad-shape-point` | `shape.path` 의 점이 `{ x, y }` 가 아니다 |
73
76
 
74
77
  ### 재질
@@ -84,12 +87,18 @@ const { errors, violations } = validate(source)
84
87
  | ---------------- | -------------------------------------------- |
85
88
  | `missing-repeat` | `sizing` 이 `'repeat'` 인데 `repeat` 가 없다 |
86
89
 
87
- ### 애니메이션
90
+ ### 움직임
88
91
 
89
- | 코드 | 언제 | 왜 |
90
- | --------------- | ------------------------ | ---------------------------------------------------------------------------------------------- |
91
- | `missing-axis` | 회전인데 `axis` 가 없다 | 어느 축으로 회전하는지 모른다 |
92
- | `missing-pivot` | 회전인데 `pivot` 이 없다 | 없으면 회전축이 부품 **자기 중심**이 된다. 교반 축이 몸체 가운데를 돌아야 하면 결과가 완전히 달라진다 |
92
+ | 코드 | 언제 | 왜 |
93
+ | ------------------------ | --------------------------------------------- | ---------------------------------------------------------------- |
94
+ | `missing-channels` | clip 에 `channels` 가 없거나 비었다 | 무엇을 움직이는지 모른다 |
95
+ | `unknown-channel-target` | `channels[].target` 이 없는 부품을 가리킨다 | 조용히 아무것도 안 움직인다 |
96
+ | `too-few-keys` | `keys` 가 둘 미만이다 | 하나짜리는 움직임이 아니라 자세 하나다. `transform` 이 그 자리다 |
97
+ | `keys-out-of-order` | 키의 시각이 앞의 것보다 크지 않다 | 표본추출이 뜻을 잃는다. 화면에서는 「이상하게 움직인다」로만 보인다 |
98
+
99
+ clip 의 이름은 부품과 같은 코드를 쓴다 — 없으면 `missing-name`, 겹치면 `duplicate-name`.
100
+ 경로 · `drive` · `interpolation` 이 목록 밖이면 `not-allowed`, 키의 시각이 음수면
101
+ `not-positive` 다.
93
102
 
94
103
  ### 예외 표면
95
104
 
@@ -118,6 +127,10 @@ const { errors, violations } = validate(source)
118
127
  | `segments-off-preset` | `segments` 가 프리셋(8 · 12 · 16 · 24 · 32 · 48) 밖이다. 값은 쓰이지만 geometry 키가 그만큼 늘어난다 |
119
128
  | `segments-ignored` | 곡면이 없는 프리미티브에 `segments` 를 주었다 |
120
129
  | `repeat-ignored` | `sizing` 이 `'repeat'` 가 아닌데 `repeat` 를 주었다 |
130
+ | `anchor-ignored` | `sizing` 이 `'scale'` 인데 `anchor` 를 주었다 — 상자와 함께 커지는 부품은 붙잡을 면이 없다 |
131
+ | `keep-round-ignored` | 단면이 원이 아닌 프리미티브에 `keepRound` 를 주었다 |
132
+ | `hollow-ignored` | 밀어 만드는 것이 아닌 프리미티브에 `shape.hollow` 를 주었다 |
133
+ | `pivot-ignored` | `rotation` 이 아닌 채널에 `pivot` 을 주었다 — 아무 일도 안 일어난다 |
121
134
 
122
135
  ### 예산
123
136
 
@@ -125,7 +138,7 @@ const { errors, violations } = validate(source)
125
138
  | --------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- |
126
139
  | `detail-level-missing` | — | 등급이 없으면 부품 수 예산을 잡을 수 없다 |
127
140
  | `too-many-parts` | `PART_LIMIT[detailLevel]` (S 3 · M 8 · L 20) | |
128
- | `too-many-animation-points` | `LIMITS.animationPoints` (2) | 포인트마다 묶음이 하나 늘고, 프레임마다 갱신이 는다 |
141
+ | `too-many-animated-parts` | `LIMITS.animatedParts` (2) | 움직이는 부품마다 묶음이 하나 늘고, 프레임마다 갱신이 는다 |
129
142
  | `too-many-material-groups` | `LIMITS.materialGroups` (3) | **자산당 draw call 이 는다.** 세는 단위는 `groupKeyOf` — 컴파일러가 합치는 단위와 같은 식이다 |
130
143
  | `too-many-transparent` | `LIMITS.transparentMaterials` (1) | 투명 재질은 별도 정렬 경로를 탄다 |
131
144
  | `too-many-always-labels` | 1 | `'always'` 라벨은 대량 배치에서 DOM·텍스처를 늘린다 |
@@ -143,7 +156,7 @@ draw call = 합쳐진 묶음 하나당 1
143
156
 
144
157
  | 무엇 | 왜 |
145
158
  | --- | --- |
146
- | 애니메이션이 걸린 부품 | 합치면 그 부품만 따로 움직일 수 없다 |
159
+ | clip 이 움직이는 부품 | 합치면 그 부품만 따로 움직일 수 없다 |
147
160
  | `sizing` 이 `scale` 이 아닌 부품 | 합친 geometry 는 변환을 하나만 받는다 |
148
161
  | `repeat` 부품 | 한 부품이 여러 mesh 다 |
149
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatiolab/figure-model",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Figure 저작 결과의 정본 형식과 검증 — 재질별로 병합되고 이름이 붙은 부품 그래프. 3D 로 저작한 형상 하나가 씬 컴포넌트로 서는 데 필요한 것을 담는다.",
5
5
  "keywords": [
6
6
  "figure",