@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 변경 기록
|
|
2
|
+
|
|
3
|
+
판 번호는 [유의적 버전](https://semver.org/lang/ko/)을 따른다. 청사진 형식 자체의 판은
|
|
4
|
+
`BLUEPRINT_VERSION` 으로 따로 매긴다 — 패키지 판을 올려도 형식이 그대로면 이 값은
|
|
5
|
+
움직이지 않는다.
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
첫 판. **계약과 검증된 코어**를 담는다. 렌더러는 들어 있지 않다.
|
|
10
|
+
|
|
11
|
+
### 담긴 것
|
|
12
|
+
|
|
13
|
+
- **형식** — `FigureSource`(편집 가능한 정본)와 `FigureBlueprint`(보드가 세우는 형태).
|
|
14
|
+
청사진 형식 판 `BLUEPRINT_VERSION = 1`
|
|
15
|
+
- **검증** — `validate()` 가 오류와 위반을 갈라 낸다. 가르는 기준은 「이 상태로 세울 수
|
|
16
|
+
있나」 하나다. 오류 24종·위반 9종이 목록으로 잠겨 있어 없는 코드를 낼 수 없다
|
|
17
|
+
- **컴파일** — `compile()` 이 정본을 재질별로 병합된 부품 그래프로 옮긴다
|
|
18
|
+
- **키** — `geometryKeyOf` · `materialKeyOf` · `groupKeyOf`. 무엇을 나눠 쓰고 무엇을
|
|
19
|
+
합치는지 정하는 식이며 `src/keys.ts` 가 유일한 정본이다
|
|
20
|
+
- **원가와 점수** — `costOf` · `scoreOf` · `costCurve` · `costCurveSvg`. 인스턴스가 늘 때
|
|
21
|
+
무엇이 늘고 무엇이 그대로인지 낸다. 점수의 근거는 이미 선언된 한도뿐이다
|
|
22
|
+
- **문서** — `docs/format.md`(형식 레퍼런스) · `docs/validation.md`(코드표) ·
|
|
23
|
+
`docs/design.md`(계약의 근거) · `docs/format-survey.md`(glTF 등을 검토한 결과)
|
|
24
|
+
|
|
25
|
+
### 아직 동작하지 않는 선언
|
|
26
|
+
|
|
27
|
+
형식에 자리는 있으나 읽어서 무언가 하는 코드가 없는 것들이 있다 — `sizing` · `repeat` ·
|
|
28
|
+
`anchor` · `labels` · `transform.rotation` · `styleKit` · `FigureInstanceModel`, 그리고
|
|
29
|
+
애니메이션 포인트의 실제 동작. 목록과 상태는 `docs/format.md` 의 「구현 상태」 절에
|
|
30
|
+
있고, 기능이 붙을 때마다 그 표를 함께 고친다.
|
|
31
|
+
|
|
32
|
+
**`sizing` 은 특히 주의한다.** `'repeat'` 는 「개수가 늘어난다」로 정의되어 있지만 아직
|
|
33
|
+
늘어나지 않는다. 지금은 네 규칙이 모두 `'scale'` 처럼 동작한다.
|
|
34
|
+
|
|
35
|
+
### 삼각형 수는 추정치다
|
|
36
|
+
|
|
37
|
+
`trianglesOf` 는 식으로 낸 값이고 실제 정점 버퍼의 삼각형 수와 맞는지 아직 확인되지
|
|
38
|
+
않았다 — 특히 `polygon` 의 cap 삼각분할이 그렇다.
|
|
39
|
+
|
|
40
|
+
### 정합성 장치
|
|
41
|
+
|
|
42
|
+
- 문서가 코드와 어긋나면 `src/docs.test.ts` 가 깨진다 — 코드 목록·열거 값·인용한
|
|
43
|
+
숫자를 모두 대조한다
|
|
44
|
+
- 검사와 컴파일이 같은 키 함수를 쓴다. 다르면 `src/contract.test.ts` 가 깨진다
|
|
45
|
+
- 커버리지 문턱 — 줄 100% · 분기 95% · 함수 100%
|
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# @hatiolab/figure-model
|
|
2
|
+
|
|
3
|
+
Figure 저작 결과의 **정본 형식과 검증**. 런타임 의존이 없다.
|
|
4
|
+
|
|
5
|
+
렌더러가 들어 있지 않다. 저작도구가 만들고, 서버가 검증해 저장하고, 뷰어·모델러가
|
|
6
|
+
소비한다 — 청사진은 실행 시점에 바깥에서 받아 이미 알고 있는 것이다.
|
|
7
|
+
|
|
8
|
+
## 무엇을 푸는 모듈인가
|
|
9
|
+
|
|
10
|
+
컴포넌트를 늘리는 일을 **개발에서 저작으로** 옮긴다. 지금은 설비 한 종류를 늘리려면
|
|
11
|
+
개발자가 클래스를 쓰고 빌드하고 배포한다. 그 일을 데이터 작성으로 바꾸려면 세 가지가
|
|
12
|
+
동시에 참이어야 한다.
|
|
13
|
+
|
|
14
|
+
| | 조건 |
|
|
15
|
+
| --- | -------------------------------------------------------------------------------- |
|
|
16
|
+
| 1 | **표현력** — 코드로 쓸 수 있던 형상을 데이터로도 쓸 수 있다 |
|
|
17
|
+
| 2 | **동등한 성능** — 데이터를 읽어 그리는 것이 코드에 박은 것만큼 빠르다 |
|
|
18
|
+
| 3 | **살아있는 컴포넌트** — 형상만이 아니라 이벤트·애니메이션·데이터 바인딩이 붙는다 |
|
|
19
|
+
|
|
20
|
+
3번이 glTF 를 탈락시킨 이유다. glTF 는 1과 2를 하지만 3의 자리가 없다. 그 검토
|
|
21
|
+
결과는 `docs/format-survey.md` 에 있다.
|
|
22
|
+
|
|
23
|
+
## 두 형태
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
FigureSource 저작 결과. 편집 가능한 정본. DB 에 저장된다.
|
|
27
|
+
FigureBlueprint 보드가 세우는 형태. 정본에서 파생되는 캐시. 저장하지 않는다.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
청사진은 **크기·상태·데이터에 반응하는 규칙이 붙은, 이름 있는 부품 그래프**다.
|
|
31
|
+
`parts[].name` 이 이 형식의 핵심이다 — `body`·`lamp`·`door` 라는 이름이 있어야
|
|
32
|
+
「lamp 색을 온도에 묶는다」, 「door 를 여는 애니메이션」, 「body 를 클릭하면」이
|
|
33
|
+
성립한다. 이름 없는 삼각형 더미로는 못 한다.
|
|
34
|
+
|
|
35
|
+
2D 와 3D 를 한 구조로 다룬다. 재질별로 묶고, 3D 는 그 묶음의 geometry 를 합쳐 draw
|
|
36
|
+
call 을 줄이고, 2D 는 윤곽을 합쳐 `fill()` 호출을 줄인다.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
자산당 묶음 수 = 재질·슬롯 조합 수 + 애니메이션 포인트 수
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 미감을 위한 제약이 그대로 성능의 근거다
|
|
43
|
+
|
|
44
|
+
격자 스냅 · 분할 프리셋(8·12·16) · 색 팔레트 · 디테일 등급(부품 3·8·20). 넷 다
|
|
45
|
+
「감각적인 로우폴리」를 위해 둔 규칙인데, 같은 규칙이 geometry 키와 material 키를
|
|
46
|
+
줄인다. 품질 규칙과 성능 규칙이 따로 있으면 서로 싸운다. 여기서는 한 벌이다.
|
|
47
|
+
|
|
48
|
+
## 쓰기
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install @hatiolab/figure-model
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { validate, compile } from '@hatiolab/figure-model'
|
|
56
|
+
|
|
57
|
+
const { errors, violations } = validate(source)
|
|
58
|
+
// errors 형식·정합성이 깨진 것. 컴파일할 수 없다.
|
|
59
|
+
// violations 정책을 넘은 것. 컴파일을 막지 않는다.
|
|
60
|
+
|
|
61
|
+
const blueprint = compile(source) // errors 가 있으면 FigureCompileError
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 얼마나 무거운가
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { costOf, describeCost, scoreOf } from '@hatiolab/figure-model'
|
|
68
|
+
|
|
69
|
+
const cost = costOf(blueprint) // 기본 100개 기준
|
|
70
|
+
describeCost(cost).forEach(line => console.log(line))
|
|
71
|
+
// SAMPLE_MIXER — 부품 6 · 삼각형 160 · 묶음 4
|
|
72
|
+
// 형상 6종 · 재질 4종
|
|
73
|
+
// 100개를 놓으면 — 삼각형 16000 · draw call 400
|
|
74
|
+
// 그래도 정점 버퍼는 4개, 재질은 4개 그대로다
|
|
75
|
+
// InstancedMesh 를 쓰면 draw call 이 4까지 내려간다
|
|
76
|
+
|
|
77
|
+
const { grade, score, tightest } = scoreOf(blueprint)
|
|
78
|
+
// grade 'A' · score 67 · tightest 는 제일 빠듯한 한도
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**그대로인 값이 곧 재활용되는 것이다.** 삼각형과 draw call 은 인스턴스에 비례하지만
|
|
82
|
+
정점 버퍼와 재질은 10,000개에서도 늘지 않는다.
|
|
83
|
+
|
|
84
|
+
점수의 근거는 **이미 선언된 한도뿐**이다(`LIMITS` · `PART_LIMIT`). 점수를 매기려고
|
|
85
|
+
지어낸 수는 없다. 자세한 것은 `docs/format.md`.
|
|
86
|
+
|
|
87
|
+
추이를 그림으로 보려면 —
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { costCurveSvg } from '@hatiolab/figure-model'
|
|
91
|
+
element.innerHTML = costCurveSvg(blueprint) // 1개 ~ 10,000개, 로그 축
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 부분만 들여오기
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import type { FigureBlueprint } from '@hatiolab/figure-model/types'
|
|
98
|
+
import { geometryKeyOf } from '@hatiolab/figure-model/blueprint'
|
|
99
|
+
import { costOf } from '@hatiolab/figure-model/cost'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 소비하는 쪽
|
|
103
|
+
|
|
104
|
+
| | |
|
|
105
|
+
| -------------- | ------------------------------------------------ |
|
|
106
|
+
| `things-scene` | `src/figure/` 가 청사진을 읽어 컴포넌트로 세운다 |
|
|
107
|
+
| 저작도구 | 정본을 만들고 고친다 |
|
|
108
|
+
| 서버 | 저장 전에 `validate()` 로 거른다 |
|
|
109
|
+
|
|
110
|
+
### things-scene 과 형식이 갈리지 않게 하는 법
|
|
111
|
+
|
|
112
|
+
**things-scene 은 이 패키지에 의존하지 않는다.** 청사진의 형식을
|
|
113
|
+
`src/figure/figure-blueprint.ts` 에 **다시 적어** 두었다 — npm 에 배포되는
|
|
114
|
+
라이브러리가 아직 배포되지 않은 패키지를 의존성으로 가질 수 없기 때문이다.
|
|
115
|
+
|
|
116
|
+
두 벌이 되면 갈릴 수 있으므로, 이 모듈이 **실제로 낸** 청사진을 things-scene 의
|
|
117
|
+
`test/fixtures/figure-blueprint-sample.json` 에 두고 그쪽 시험이 읽는다. 형식을
|
|
118
|
+
바꿨으면 표본을 다시 뽑는다.
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
# things-scene 에서
|
|
122
|
+
node test/fixtures/emit-figure-blueprint.mjs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
그때 그쪽 시험이 깨지면 읽는 쪽 계약도 함께 고친다. 이 패키지가 배포되고 나면
|
|
126
|
+
`import type` 한 벌로 줄일 수 있다.
|
|
127
|
+
|
|
128
|
+
## 명령
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
npm test 기능 시험
|
|
132
|
+
npm run test:coverage 커버리지 (문턱: 줄 100% · 분기 95% · 함수 100%)
|
|
133
|
+
npm run check 타입 검사 (시험·벤치까지 포함)
|
|
134
|
+
npm run bench 부하 비교 — 컴포넌트 트리로 세우는 방식과 견준다
|
|
135
|
+
npm run build dist 로 빌드 (시험·벤치는 빠진다)
|
|
136
|
+
npm run format 서식 정리
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 요구 환경
|
|
140
|
+
|
|
141
|
+
| | |
|
|
142
|
+
| ---- | ------------------------------------------------------------------------ |
|
|
143
|
+
| 소비 | Node 20 이상. ESM 전용이고 CommonJS 산출물은 내지 않는다. |
|
|
144
|
+
| 개발 | Node 22.6 이상 — `npm test` 가 TypeScript 를 그대로 돌린다(타입 벗기기). |
|
|
145
|
+
|
|
146
|
+
`enum` 은 쓰지 않는다. Node 의 타입 벗기기는 코드 생성이 필요한 문법을 실행하지
|
|
147
|
+
못한다.
|
|
148
|
+
|
|
149
|
+
## 의존
|
|
150
|
+
|
|
151
|
+
**런타임 의존이 없다.** 브라우저에서도 그대로 돈다.
|
|
152
|
+
|
|
153
|
+
devDependency 는 셋 — `typescript`(타입 검사·`.d.ts` 생성), `@types/node`(시험과
|
|
154
|
+
벤치가 `node:test`·`node:zlib` 을 쓴다), `prettier`(서식). 셋 다 `dist` 에 들어가지
|
|
155
|
+
않는다.
|
|
156
|
+
|
|
157
|
+
## 문서
|
|
158
|
+
|
|
159
|
+
- `docs/format.md` — **형식 레퍼런스.** 모든 필드와, 선언은 있으나 아직 동작하지 않는 것
|
|
160
|
+
- `docs/validation.md` — **오류·위반 코드표.** 코드마다 언제 나는지와 왜 그렇게 갈랐는지
|
|
161
|
+
- `docs/design.md` — 계약과 그 근거
|
|
162
|
+
- `docs/format-survey.md` — glTF·USD·three.js toJSON 등을 검토한 결과와 자체 형식의 근거
|
|
163
|
+
- 저작도구 전체 설계는 `things-factory/docs/design/component-authoring-tool.md`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ValidateResult } from './validate.ts';
|
|
2
|
+
export { geometryKeyOf, materialKeyOf, usesSegments } from './keys.ts';
|
|
3
|
+
import { type FigureBlueprint, type FigurePart } from './types.ts';
|
|
4
|
+
/** 컴파일할 수 없을 때 던진다. 조용히 빈 청사진을 돌려주지 않는다. */
|
|
5
|
+
export declare class FigureCompileError extends Error {
|
|
6
|
+
readonly errors: ValidateResult['errors'];
|
|
7
|
+
constructor(errors: ValidateResult['errors']);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 삼각형 수 추정.
|
|
11
|
+
*
|
|
12
|
+
* 근거는 things-scene 의 geometry 생성 인자다.
|
|
13
|
+
*
|
|
14
|
+
* cube · wall BoxGeometry 면 6 × 2 = 12
|
|
15
|
+
* cylinder CylinderGeometry(seg) 측면 2·seg + 캡 2·seg = 4·seg
|
|
16
|
+
* sphere SphereGeometry(w, h) 2·w·(h−1) 극점 줄은 삼각형 하나씩
|
|
17
|
+
* rect ExtrudeGeometry 캡 2×2 + 측면 4×2 = 12
|
|
18
|
+
* polygon(n 점) ExtrudeGeometry 캡 2·(n−2) + 측면 2·n
|
|
19
|
+
*
|
|
20
|
+
* 추정이다. 베벨과 라운드 코너는 세지 않는다 — 예산의 자릿수를 보는 값이므로
|
|
21
|
+
* 정확도보다 일관성이 중요하다.
|
|
22
|
+
*/
|
|
23
|
+
export declare function trianglesOf(part: FigurePart): number;
|
|
24
|
+
/**
|
|
25
|
+
* 정본을 청사진으로 컴파일한다.
|
|
26
|
+
*
|
|
27
|
+
* `errors` 가 있으면 던진다 — 세울 수 없는 것을 반쯤 세운 청사진으로 돌려주면
|
|
28
|
+
* 어디서 잘못됐는지 알 수 없다. `violations` 는 청사진에 담아 돌려준다. 정책 위반은
|
|
29
|
+
* 컴파일을 막지 않고 숫자로 보인다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function compile(source: unknown): FigureBlueprint;
|
|
32
|
+
//# sourceMappingURL=blueprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blueprint.d.ts","sourceRoot":"","sources":["../src/blueprint.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAI7D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACtE,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,UAAU,EAGhB,MAAM,YAAY,CAAA;AAEnB,4CAA4C;AAC5C,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAA;gBAE7B,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC;CAS7C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAoBpD;AAoBD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAsExD"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* 정본 → 청사진 컴파일.
|
|
5
|
+
*
|
|
6
|
+
* 정본은 편집을 위한 형식이다. 보드가 그것을 그대로 세우면 부품마다 mesh 가 생겨서
|
|
7
|
+
* 자산 500 개 × 부품 8 개 = 4000 draw call 이 된다. 청사진은 그것을 줄인 형식이다.
|
|
8
|
+
*
|
|
9
|
+
* 핵심은 **병합**이다. 같은 재질·슬롯을 쓰고 애니메이션이 없는 부품을 한 묶음으로
|
|
10
|
+
* 묶는다. 3D 는 그 묶음의 geometry 를 합치고, 2D 는 윤곽을 한 path 로 합쳐 한 번에
|
|
11
|
+
* 채운다. 병목이 양쪽 다 묶음 수여서 같은 병합이 함께 값을 한다.
|
|
12
|
+
*
|
|
13
|
+
* 자산당 묶음 수 = 재질·슬롯 조합 수 + 애니메이션 포인트 수
|
|
14
|
+
*
|
|
15
|
+
* 애니메이션이 걸린 부품은 합치지 않는다. 합치면 그 부품만 따로 움직일 수 없다.
|
|
16
|
+
*/
|
|
17
|
+
import { validate } from "./validate.js";
|
|
18
|
+
import { geometryKeyOf, groupKeyOf, materialKeyOf, usesSegments } from "./keys.js";
|
|
19
|
+
// 키는 keys.ts 가 정본이다. 소비처가 쓰던 이름 그대로 다시 내보낸다.
|
|
20
|
+
export { geometryKeyOf, materialKeyOf, usesSegments } from "./keys.js";
|
|
21
|
+
import { BLUEPRINT_VERSION } from "./types.js";
|
|
22
|
+
/** 컴파일할 수 없을 때 던진다. 조용히 빈 청사진을 돌려주지 않는다. */
|
|
23
|
+
export class FigureCompileError extends Error {
|
|
24
|
+
errors;
|
|
25
|
+
constructor(errors) {
|
|
26
|
+
const head = errors
|
|
27
|
+
.slice(0, 3)
|
|
28
|
+
.map(e => `${e.path || '(root)'}: ${e.message}`)
|
|
29
|
+
.join(' / ');
|
|
30
|
+
super(`정본을 컴파일할 수 없다 — ${errors.length}건. ${head}`);
|
|
31
|
+
this.name = 'FigureCompileError';
|
|
32
|
+
this.errors = errors;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 삼각형 수 추정.
|
|
37
|
+
*
|
|
38
|
+
* 근거는 things-scene 의 geometry 생성 인자다.
|
|
39
|
+
*
|
|
40
|
+
* cube · wall BoxGeometry 면 6 × 2 = 12
|
|
41
|
+
* cylinder CylinderGeometry(seg) 측면 2·seg + 캡 2·seg = 4·seg
|
|
42
|
+
* sphere SphereGeometry(w, h) 2·w·(h−1) 극점 줄은 삼각형 하나씩
|
|
43
|
+
* rect ExtrudeGeometry 캡 2×2 + 측면 4×2 = 12
|
|
44
|
+
* polygon(n 점) ExtrudeGeometry 캡 2·(n−2) + 측면 2·n
|
|
45
|
+
*
|
|
46
|
+
* 추정이다. 베벨과 라운드 코너는 세지 않는다 — 예산의 자릿수를 보는 값이므로
|
|
47
|
+
* 정확도보다 일관성이 중요하다.
|
|
48
|
+
*/
|
|
49
|
+
export function trianglesOf(part) {
|
|
50
|
+
const seg = part.segments ?? 12;
|
|
51
|
+
switch (part.primitive) {
|
|
52
|
+
case 'cube':
|
|
53
|
+
case 'wall':
|
|
54
|
+
return 12;
|
|
55
|
+
case 'cylinder':
|
|
56
|
+
return 4 * seg;
|
|
57
|
+
case 'sphere': {
|
|
58
|
+
const h = Math.max(2, Math.round(seg / 2));
|
|
59
|
+
return 2 * seg * (h - 1);
|
|
60
|
+
}
|
|
61
|
+
case 'rect':
|
|
62
|
+
return 12;
|
|
63
|
+
case 'polygon': {
|
|
64
|
+
const n = part.shape?.path?.length ?? 3;
|
|
65
|
+
return 2 * (n - 2) + 2 * n;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** `sizing` 이 없으면 무엇으로 보나 — 비례 확대가 가장 예측 가능하다. */
|
|
70
|
+
const DEFAULT_SIZING = 'scale';
|
|
71
|
+
function memberOf(part) {
|
|
72
|
+
const member = {
|
|
73
|
+
name: part.name,
|
|
74
|
+
primitive: part.primitive,
|
|
75
|
+
geometryKey: geometryKeyOf(part),
|
|
76
|
+
transform: part.transform,
|
|
77
|
+
sizing: part.sizing ?? DEFAULT_SIZING,
|
|
78
|
+
triangles: trianglesOf(part)
|
|
79
|
+
};
|
|
80
|
+
if (usesSegments(part.primitive) && part.segments !== undefined)
|
|
81
|
+
member.segments = part.segments;
|
|
82
|
+
if (part.repeat !== undefined)
|
|
83
|
+
member.repeat = part.repeat;
|
|
84
|
+
if (part.shape !== undefined)
|
|
85
|
+
member.shape = part.shape;
|
|
86
|
+
return member;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 정본을 청사진으로 컴파일한다.
|
|
90
|
+
*
|
|
91
|
+
* `errors` 가 있으면 던진다 — 세울 수 없는 것을 반쯤 세운 청사진으로 돌려주면
|
|
92
|
+
* 어디서 잘못됐는지 알 수 없다. `violations` 는 청사진에 담아 돌려준다. 정책 위반은
|
|
93
|
+
* 컴파일을 막지 않고 숫자로 보인다.
|
|
94
|
+
*/
|
|
95
|
+
export function compile(source) {
|
|
96
|
+
const { errors, violations } = validate(source);
|
|
97
|
+
if (errors.length > 0)
|
|
98
|
+
throw new FigureCompileError(errors);
|
|
99
|
+
const src = source;
|
|
100
|
+
const groups = [];
|
|
101
|
+
const points = [];
|
|
102
|
+
const labels = [];
|
|
103
|
+
const byKey = new Map();
|
|
104
|
+
for (const part of src.parts) {
|
|
105
|
+
if (part.label !== undefined) {
|
|
106
|
+
labels.push({ ...part.label, at: part.name });
|
|
107
|
+
}
|
|
108
|
+
if (part.animation !== undefined) {
|
|
109
|
+
// 애니메이션이 걸린 부품은 단독 묶음이다 — 합치면 따로 움직일 수 없다
|
|
110
|
+
const point = { name: part.name, kind: part.animation.kind };
|
|
111
|
+
if (part.animation.axis !== undefined)
|
|
112
|
+
point.axis = part.animation.axis;
|
|
113
|
+
if (part.animation.pivot !== undefined)
|
|
114
|
+
point.pivot = part.animation.pivot;
|
|
115
|
+
points.push(point);
|
|
116
|
+
const group = {
|
|
117
|
+
materialKey: materialKeyOf(part),
|
|
118
|
+
material: part.material,
|
|
119
|
+
animated: true,
|
|
120
|
+
members: [memberOf(part)]
|
|
121
|
+
};
|
|
122
|
+
if (part.slot !== undefined)
|
|
123
|
+
group.slot = part.slot;
|
|
124
|
+
groups.push(group);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const key = groupKeyOf(part);
|
|
128
|
+
const existing = byKey.get(key);
|
|
129
|
+
if (existing) {
|
|
130
|
+
existing.members.push(memberOf(part));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const group = {
|
|
134
|
+
materialKey: materialKeyOf(part),
|
|
135
|
+
material: part.material,
|
|
136
|
+
animated: false,
|
|
137
|
+
members: [memberOf(part)]
|
|
138
|
+
};
|
|
139
|
+
if (part.slot !== undefined)
|
|
140
|
+
group.slot = part.slot;
|
|
141
|
+
byKey.set(key, group);
|
|
142
|
+
groups.push(group);
|
|
143
|
+
}
|
|
144
|
+
const triangles = groups.reduce((sum, g) => sum + g.members.reduce((s, m) => s + m.triangles, 0), 0);
|
|
145
|
+
const blueprint = {
|
|
146
|
+
version: BLUEPRINT_VERSION,
|
|
147
|
+
type: src.type,
|
|
148
|
+
base: src.base,
|
|
149
|
+
budget: { triangles, groups: groups.length },
|
|
150
|
+
groups,
|
|
151
|
+
points,
|
|
152
|
+
labels,
|
|
153
|
+
violations
|
|
154
|
+
};
|
|
155
|
+
if (src.anchor !== undefined)
|
|
156
|
+
blueprint.anchor = src.anchor;
|
|
157
|
+
// 등급을 실어 보낸다 — 부품 수 한도가 여기서 나오므로, 없으면 소비처가 예산을
|
|
158
|
+
// 판단할 수 없다.
|
|
159
|
+
if (src.detailLevel !== undefined)
|
|
160
|
+
blueprint.detailLevel = src.detailLevel;
|
|
161
|
+
return blueprint;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=blueprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blueprint.js","sourceRoot":"","sources":["../src/blueprint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 원가 추이 — 인스턴스가 1 개에서 10,000 개로 갈 때 무엇이 늘고 무엇이 그대로인가.
|
|
3
|
+
*
|
|
4
|
+
* 표 하나로는 그 대비가 안 보인다. **같은 그림 안에서 오르는 선과 평평한 선을 함께
|
|
5
|
+
* 봐야** 「재활용된다」는 말이 눈에 들어온다.
|
|
6
|
+
*
|
|
7
|
+
* 삼각형 · draw call 인스턴스에 비례해 오른다
|
|
8
|
+
* 정점 버퍼 · 재질 10,000 개에서도 그대로다 ← 재활용되는 것
|
|
9
|
+
* InstancedMesh 의 draw call 평평하다 ← 아직 얻지 않은 몫
|
|
10
|
+
*
|
|
11
|
+
* 의존을 만들지 않으려고 SVG 를 문자열로 낸다. 브라우저에서도 node 에서도 그대로 돈다.
|
|
12
|
+
* 색은 다섯 계열만 쓰고 축과 글자는 `currentColor` 라서 밝은 테마·어두운 테마 양쪽에서
|
|
13
|
+
* 읽힌다.
|
|
14
|
+
*/
|
|
15
|
+
import type { FigureCostAt } from './cost.ts';
|
|
16
|
+
import type { FigureBlueprint } from './types.ts';
|
|
17
|
+
/** 추이의 한 점. */
|
|
18
|
+
export interface CostCurvePoint extends FigureCostAt {
|
|
19
|
+
}
|
|
20
|
+
export interface CostCurveOptions {
|
|
21
|
+
/** 시작 인스턴스 수. 기본 1 */
|
|
22
|
+
from?: number;
|
|
23
|
+
/** 끝 인스턴스 수. 기본 10000 */
|
|
24
|
+
to?: number;
|
|
25
|
+
}
|
|
26
|
+
/** 인스턴스 수를 훑으며 원가를 낸다. */
|
|
27
|
+
export declare function costCurve(blueprint: FigureBlueprint, options?: CostCurveOptions): CostCurvePoint[];
|
|
28
|
+
/**
|
|
29
|
+
* 추이를 SVG 문자열로 낸다.
|
|
30
|
+
*
|
|
31
|
+
* 두 축 모두 로그다. 선형 축으로 그리면 10,000 쪽 값이 화면을 다 먹어 1 개 근처가
|
|
32
|
+
* 뭉개지고, 정작 보여 줄 것 — **평평한 선과 오르는 선의 대비** — 가 안 보인다.
|
|
33
|
+
*/
|
|
34
|
+
export declare function costCurveSvg(blueprint: FigureBlueprint, options?: CostCurveOptions): string;
|
|
35
|
+
//# sourceMappingURL=cost-chart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-chart.d.ts","sourceRoot":"","sources":["../src/cost-chart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,eAAe;AACf,MAAM,WAAW,cAAe,SAAQ,YAAY;CAAG;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,sBAAsB;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yBAAyB;IACzB,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAsBD,0BAA0B;AAC1B,wBAAgB,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,OAAO,GAAE,gBAAqB,GAAG,cAAc,EAAE,CAItG;AA6CD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,eAAe,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAuG/F"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 원가 추이 — 인스턴스가 1 개에서 10,000 개로 갈 때 무엇이 늘고 무엇이 그대로인가.
|
|
3
|
+
*
|
|
4
|
+
* 표 하나로는 그 대비가 안 보인다. **같은 그림 안에서 오르는 선과 평평한 선을 함께
|
|
5
|
+
* 봐야** 「재활용된다」는 말이 눈에 들어온다.
|
|
6
|
+
*
|
|
7
|
+
* 삼각형 · draw call 인스턴스에 비례해 오른다
|
|
8
|
+
* 정점 버퍼 · 재질 10,000 개에서도 그대로다 ← 재활용되는 것
|
|
9
|
+
* InstancedMesh 의 draw call 평평하다 ← 아직 얻지 않은 몫
|
|
10
|
+
*
|
|
11
|
+
* 의존을 만들지 않으려고 SVG 를 문자열로 낸다. 브라우저에서도 node 에서도 그대로 돈다.
|
|
12
|
+
* 색은 다섯 계열만 쓰고 축과 글자는 `currentColor` 라서 밝은 테마·어두운 테마 양쪽에서
|
|
13
|
+
* 읽힌다.
|
|
14
|
+
*/
|
|
15
|
+
import { costOf } from "./cost.js";
|
|
16
|
+
/**
|
|
17
|
+
* 1-2-5 간격으로 자릿수를 훑는다.
|
|
18
|
+
*
|
|
19
|
+
* 로그 축에서 눈에 고르게 놓이는 값들이다. 1·2·5·10·20·50·100 … 등비로 늘리면 자릿수
|
|
20
|
+
* 하나에 점이 셋씩 놓여 곡선이 매끄럽다.
|
|
21
|
+
*/
|
|
22
|
+
function decadeSteps(from, to) {
|
|
23
|
+
const out = [];
|
|
24
|
+
let decade = 1;
|
|
25
|
+
while (decade <= to) {
|
|
26
|
+
for (const m of [1, 2, 5]) {
|
|
27
|
+
const v = decade * m;
|
|
28
|
+
if (v >= from && v <= to)
|
|
29
|
+
out.push(v);
|
|
30
|
+
}
|
|
31
|
+
decade *= 10;
|
|
32
|
+
}
|
|
33
|
+
if (out.length === 0 || out[out.length - 1] !== to)
|
|
34
|
+
out.push(to);
|
|
35
|
+
return [...new Set(out)].sort((a, b) => a - b);
|
|
36
|
+
}
|
|
37
|
+
/** 인스턴스 수를 훑으며 원가를 낸다. */
|
|
38
|
+
export function costCurve(blueprint, options = {}) {
|
|
39
|
+
const from = Math.max(1, Math.floor(options.from ?? 1));
|
|
40
|
+
const to = Math.max(from, Math.floor(options.to ?? 10000));
|
|
41
|
+
return decadeSteps(from, to).map(n => costOf(blueprint, n).at);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 그리는 계열 넷.
|
|
45
|
+
*
|
|
46
|
+
* `drawCallsIfInstanced` 는 선으로 그리지 않는다 — 구조적으로 `geometries` 와 **항상
|
|
47
|
+
* 같은 값**이기 때문이다(둘 다 묶음 수). 절대 갈릴 수 없는 선을 둘 그리면 하나가 다른
|
|
48
|
+
* 하나를 덮어 정보가 오히려 준다. 대신 그 뜻을 이름에 담았다.
|
|
49
|
+
*
|
|
50
|
+
* `materials` 는 `geometries` 와 같을 수도 다를 수도 있다(재질이 슬롯으로 더 갈리면
|
|
51
|
+
* 묶음이 더 많다). 겹칠 때 아래 선이 보이도록 점선으로 마지막에 그린다.
|
|
52
|
+
*/
|
|
53
|
+
const SERIES = [
|
|
54
|
+
{ key: 'triangles', label: '삼각형', color: '#e5534b', flat: false },
|
|
55
|
+
{ key: 'drawCalls', label: 'draw call', color: '#e09b13', flat: false },
|
|
56
|
+
{ key: 'geometries', label: '정점 버퍼', color: '#4a9eff', flat: true },
|
|
57
|
+
{ key: 'materials', label: '재질', color: '#a371f7', flat: true, dashed: true }
|
|
58
|
+
];
|
|
59
|
+
const W = 680;
|
|
60
|
+
const H = 380;
|
|
61
|
+
const PAD = { top: 52, right: 208, bottom: 44, left: 62 };
|
|
62
|
+
const log10 = (v) => Math.log(Math.max(v, 1)) / Math.LN10;
|
|
63
|
+
function escapeText(s) {
|
|
64
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
65
|
+
}
|
|
66
|
+
/** 1, 10, 100 … 형태로 짧게. */
|
|
67
|
+
function shortNumber(v) {
|
|
68
|
+
if (v >= 1_000_000)
|
|
69
|
+
return `${v / 1_000_000}M`;
|
|
70
|
+
if (v >= 1_000)
|
|
71
|
+
return `${v / 1_000}k`;
|
|
72
|
+
return String(v);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 추이를 SVG 문자열로 낸다.
|
|
76
|
+
*
|
|
77
|
+
* 두 축 모두 로그다. 선형 축으로 그리면 10,000 쪽 값이 화면을 다 먹어 1 개 근처가
|
|
78
|
+
* 뭉개지고, 정작 보여 줄 것 — **평평한 선과 오르는 선의 대비** — 가 안 보인다.
|
|
79
|
+
*/
|
|
80
|
+
export function costCurveSvg(blueprint, options = {}) {
|
|
81
|
+
const points = costCurve(blueprint, options);
|
|
82
|
+
if (points.length === 0)
|
|
83
|
+
return '';
|
|
84
|
+
const xs = points.map(p => p.instances);
|
|
85
|
+
const minX = xs[0];
|
|
86
|
+
const maxX = xs[xs.length - 1];
|
|
87
|
+
let maxY = 1;
|
|
88
|
+
for (const p of points) {
|
|
89
|
+
for (const s of SERIES)
|
|
90
|
+
maxY = Math.max(maxY, p[s.key]);
|
|
91
|
+
}
|
|
92
|
+
const plotW = W - PAD.left - PAD.right;
|
|
93
|
+
const plotH = H - PAD.top - PAD.bottom;
|
|
94
|
+
const spanX = Math.max(log10(maxX) - log10(minX), 1e-9);
|
|
95
|
+
const spanY = Math.max(log10(maxY), 1e-9);
|
|
96
|
+
const px = (v) => PAD.left + ((log10(v) - log10(minX)) / spanX) * plotW;
|
|
97
|
+
const py = (v) => PAD.top + plotH - (log10(v) / spanY) * plotH;
|
|
98
|
+
const parts = [];
|
|
99
|
+
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" ` +
|
|
100
|
+
`font-family="system-ui, sans-serif" font-size="11" fill="currentColor">`);
|
|
101
|
+
parts.push(`<text x="${PAD.left}" y="22" font-size="13" font-weight="600">` +
|
|
102
|
+
`${escapeText(blueprint.type)} — 인스턴스가 늘 때의 원가</text>`, `<text x="${PAD.left}" y="38" opacity="0.6">` +
|
|
103
|
+
`평평한 선은 인스턴스가 늘어도 그대로다 — 그것이 재활용되는 것이다</text>`);
|
|
104
|
+
// 세로 눈금 — 자릿수마다
|
|
105
|
+
for (let e = 0; Math.pow(10, e) <= maxY * 1.001; e++) {
|
|
106
|
+
const v = Math.pow(10, e);
|
|
107
|
+
const y = py(v);
|
|
108
|
+
parts.push(`<line x1="${PAD.left}" y1="${y.toFixed(1)}" x2="${(PAD.left + plotW).toFixed(1)}" y2="${y.toFixed(1)}" ` +
|
|
109
|
+
`stroke="currentColor" stroke-opacity="0.12"/>`, `<text x="${PAD.left - 8}" y="${(y + 3.5).toFixed(1)}" text-anchor="end" opacity="0.55">${shortNumber(v)}</text>`);
|
|
110
|
+
}
|
|
111
|
+
// 가로 눈금 — 자릿수마다
|
|
112
|
+
for (let e = 0; Math.pow(10, e) <= maxX * 1.001; e++) {
|
|
113
|
+
const v = Math.pow(10, e);
|
|
114
|
+
if (v < minX)
|
|
115
|
+
continue;
|
|
116
|
+
const x = px(v);
|
|
117
|
+
parts.push(`<line x1="${x.toFixed(1)}" y1="${PAD.top}" x2="${x.toFixed(1)}" y2="${(PAD.top + plotH).toFixed(1)}" ` +
|
|
118
|
+
`stroke="currentColor" stroke-opacity="0.12"/>`, `<text x="${x.toFixed(1)}" y="${(PAD.top + plotH + 16).toFixed(1)}" text-anchor="middle" opacity="0.55">` +
|
|
119
|
+
`${shortNumber(v)}</text>`);
|
|
120
|
+
}
|
|
121
|
+
parts.push(`<text x="${(PAD.left + plotW / 2).toFixed(1)}" y="${H - 8}" text-anchor="middle" opacity="0.55">인스턴스 수</text>`);
|
|
122
|
+
// 선
|
|
123
|
+
for (const s of SERIES) {
|
|
124
|
+
const d = points
|
|
125
|
+
.map((p, i) => `${i === 0 ? 'M' : 'L'}${px(p.instances).toFixed(1)},${py(p[s.key]).toFixed(1)}`)
|
|
126
|
+
.join(' ');
|
|
127
|
+
parts.push(`<path d="${d}" fill="none" stroke="${s.color}" stroke-width="2" ` +
|
|
128
|
+
`stroke-linejoin="round"${s.dashed ? ' stroke-dasharray="3 7"' : ''}/>`);
|
|
129
|
+
}
|
|
130
|
+
// 범례 — 끝 값을 함께 보인다. 「10,000 개면 얼마인가」가 바로 읽힌다.
|
|
131
|
+
const last = points[points.length - 1];
|
|
132
|
+
parts.push(`<g transform="translate(${W - PAD.right + 14}, ${PAD.top - 6})">`);
|
|
133
|
+
parts.push(`<text x="0" y="0" font-weight="600">${shortNumber(last.instances)} 개일 때</text>`);
|
|
134
|
+
SERIES.forEach((s, i) => {
|
|
135
|
+
const y = 24 + i * 34;
|
|
136
|
+
parts.push(`<line x1="0" y1="${y - 4}" x2="18" y2="${y - 4}" stroke="${s.color}" stroke-width="2"` +
|
|
137
|
+
`${s.dashed ? ' stroke-dasharray="3 7"' : ''}/>`, `<text x="24" y="${y}" opacity="0.85">${escapeText(s.label)}</text>`, `<text x="24" y="${y + 12}" font-weight="600" fill="${s.color}">` +
|
|
138
|
+
`${last[s.key].toLocaleString('en-US')}${s.flat ? ' · 그대로' : ''}</text>`);
|
|
139
|
+
});
|
|
140
|
+
// 겹쳐 보이는 것을 말로 밝힌다 — 값이 같으면 선이 포개진다.
|
|
141
|
+
const flatValues = new Set(SERIES.filter(x => x.flat).map(x => last[x.key]));
|
|
142
|
+
const note = flatValues.size === 1
|
|
143
|
+
? `평평한 선들이 ${[...flatValues][0]} 에서 포개져 있다`
|
|
144
|
+
: 'InstancedMesh 를 쓰면 draw call 이 정점 버퍼 수까지 내려간다';
|
|
145
|
+
parts.push(`<text x="0" y="${24 + SERIES.length * 34 + 6}" opacity="0.55">${escapeText(note)}</text>`, `<text x="0" y="${24 + SERIES.length * 34 + 20}" opacity="0.55">InstancedMesh 면 draw call = 정점 버퍼 수</text>`);
|
|
146
|
+
parts.push('</g>');
|
|
147
|
+
parts.push('</svg>');
|
|
148
|
+
return parts.join('\n');
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=cost-chart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-chart.js","sourceRoot":"","sources":["../src/cost-chart.ts"],"names":[],"mappings":"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"}
|