@things-factory/figure-ui 10.1.43 → 10.1.45
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/client/modeller/figure-v3-links.ts +303 -0
- package/client/modeller/modeller-route.ts +6 -21
- package/client/modeller/v3-editor.ts +44 -3
- package/client/modeller/v3-gizmo.ts +13 -2
- package/client/modeller/v3-links.ts +138 -0
- package/client/pages/figure-v3-modeller-page.ts +2 -5
- package/client/route.ts +2 -6
- package/dist-client/modeller/figure-v3-links.d.ts +42 -0
- package/dist-client/modeller/figure-v3-links.js +306 -0
- package/dist-client/modeller/figure-v3-links.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +4 -11
- package/dist-client/modeller/modeller-route.js +5 -22
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/v3-editor.d.ts +5 -0
- package/dist-client/modeller/v3-editor.js +46 -2
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.js +11 -1
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +54 -0
- package/dist-client/modeller/v3-links.js +110 -0
- package/dist-client/modeller/v3-links.js.map +1 -0
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -5
- package/dist-client/pages/figure-v3-modeller-page.js +2 -5
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +2 -5
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +0 -88
- package/test/figure-source.test.ts +0 -102
- package/test/i18n-prefix-guard.test.ts +0 -24
- package/test/modeller-route.test.ts +14 -25
- package/test/part-edits.test.ts +0 -7
- package/test/scene-budget.test.ts +0 -7
- package/test/v3-gizmo.test.ts +10 -0
- package/test/v3-links.test.ts +123 -0
- package/things-factory.config.js +2 -5
- package/translations/en.json +6 -0
- package/translations/ja.json +11 -1
- package/translations/ko.json +6 -0
- package/translations/ms.json +11 -1
- package/translations/zh.json +11 -1
- package/client/modeller/angle-dial.ts +0 -70
- package/client/modeller/base-box.ts +0 -175
- package/client/modeller/figure-animations.ts +0 -1249
- package/client/modeller/figure-canvas.ts +0 -567
- package/client/modeller/figure-capabilities.ts +0 -379
- package/client/modeller/figure-inspector.ts +0 -2208
- package/client/modeller/figure-parts.ts +0 -479
- package/client/modeller/figure-preview.ts +0 -598
- package/client/modeller/figure-report.ts +0 -966
- package/client/modeller/figure-settings.ts +0 -328
- package/client/modeller/figure-side.ts +0 -272
- package/client/modeller/figure-thumbnail.ts +0 -123
- package/client/modeller/figure-view.ts +0 -152
- package/client/modeller/joint-edits.ts +0 -262
- package/client/modeller/joint-gizmo.ts +0 -180
- package/client/modeller/maturity.ts +0 -288
- package/client/modeller/new-figure-entry.ts +0 -4
- package/client/modeller/pose-defaults.ts +0 -67
- package/client/pages/figure-modeller-page.ts +0 -2082
- package/dist-client/modeller/angle-dial.d.ts +0 -32
- package/dist-client/modeller/angle-dial.js +0 -41
- package/dist-client/modeller/angle-dial.js.map +0 -1
- package/dist-client/modeller/base-box.d.ts +0 -71
- package/dist-client/modeller/base-box.js +0 -110
- package/dist-client/modeller/base-box.js.map +0 -1
- package/dist-client/modeller/figure-animations.d.ts +0 -159
- package/dist-client/modeller/figure-animations.js +0 -1132
- package/dist-client/modeller/figure-animations.js.map +0 -1
- package/dist-client/modeller/figure-canvas.d.ts +0 -150
- package/dist-client/modeller/figure-canvas.js +0 -506
- package/dist-client/modeller/figure-canvas.js.map +0 -1
- package/dist-client/modeller/figure-capabilities.d.ts +0 -42
- package/dist-client/modeller/figure-capabilities.js +0 -349
- package/dist-client/modeller/figure-capabilities.js.map +0 -1
- package/dist-client/modeller/figure-inspector.d.ts +0 -329
- package/dist-client/modeller/figure-inspector.js +0 -2095
- package/dist-client/modeller/figure-inspector.js.map +0 -1
- package/dist-client/modeller/figure-parts.d.ts +0 -64
- package/dist-client/modeller/figure-parts.js +0 -478
- package/dist-client/modeller/figure-parts.js.map +0 -1
- package/dist-client/modeller/figure-preview.d.ts +0 -138
- package/dist-client/modeller/figure-preview.js +0 -549
- package/dist-client/modeller/figure-preview.js.map +0 -1
- package/dist-client/modeller/figure-report.d.ts +0 -165
- package/dist-client/modeller/figure-report.js +0 -952
- package/dist-client/modeller/figure-report.js.map +0 -1
- package/dist-client/modeller/figure-settings.d.ts +0 -39
- package/dist-client/modeller/figure-settings.js +0 -317
- package/dist-client/modeller/figure-settings.js.map +0 -1
- package/dist-client/modeller/figure-side.d.ts +0 -78
- package/dist-client/modeller/figure-side.js +0 -317
- package/dist-client/modeller/figure-side.js.map +0 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +0 -3
- package/dist-client/modeller/figure-thumbnail.js +0 -95
- package/dist-client/modeller/figure-thumbnail.js.map +0 -1
- package/dist-client/modeller/figure-view.d.ts +0 -118
- package/dist-client/modeller/figure-view.js +0 -78
- package/dist-client/modeller/figure-view.js.map +0 -1
- package/dist-client/modeller/joint-edits.d.ts +0 -49
- package/dist-client/modeller/joint-edits.js +0 -223
- package/dist-client/modeller/joint-edits.js.map +0 -1
- package/dist-client/modeller/joint-gizmo.d.ts +0 -34
- package/dist-client/modeller/joint-gizmo.js +0 -158
- package/dist-client/modeller/joint-gizmo.js.map +0 -1
- package/dist-client/modeller/maturity.d.ts +0 -55
- package/dist-client/modeller/maturity.js +0 -212
- package/dist-client/modeller/maturity.js.map +0 -1
- package/dist-client/modeller/new-figure-entry.d.ts +0 -2
- package/dist-client/modeller/new-figure-entry.js +0 -5
- package/dist-client/modeller/new-figure-entry.js.map +0 -1
- package/dist-client/modeller/pose-defaults.d.ts +0 -9
- package/dist-client/modeller/pose-defaults.js +0 -35
- package/dist-client/modeller/pose-defaults.js.map +0 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -419
- package/dist-client/pages/figure-modeller-page.js +0 -2060
- package/dist-client/pages/figure-modeller-page.js.map +0 -1
- package/test/angle-dial.test.ts +0 -78
- package/test/base-box.test.ts +0 -235
- package/test/joint-edits.test.ts +0 -165
- package/test/joint-gizmo.test.ts +0 -174
- package/test/joint-needle.test.ts +0 -162
- package/test/modeller-undo-redo.test.ts +0 -33
- package/test/new-figure-entry.test.ts +0 -15
- package/test/pose-defaults.test.ts +0 -71
- package/test/preview-board-depth.test.ts +0 -27
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { LIMITS, PART_LIMIT, SEGMENT_PRESETS, materialKeyOf } from '@hatiolab/figure-model';
|
|
2
|
-
import { capacityFrom } from './scene-budget.js';
|
|
3
|
-
/** 한 도면에 이만큼 놓는 것을 본다. 설계 문서 §4.15 의 「자산 500 개」. */
|
|
4
|
-
const BOARD_ASSETS = 500;
|
|
5
|
-
/**
|
|
6
|
-
* 여섯 축을 측정한다.
|
|
7
|
-
*
|
|
8
|
-
* 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다 — 그것이 이
|
|
9
|
-
* 리포트의 존재 이유다.
|
|
10
|
-
*/
|
|
11
|
-
export function maturityOf(source, cost, score) {
|
|
12
|
-
return [scale(cost), consistency(source, cost), reuse(score), detail(source, cost), reactive(source), readable(source, cost)];
|
|
13
|
-
}
|
|
14
|
-
/** 규모 — 한 도면에 얼마나 놓을 수 있나. */
|
|
15
|
-
function scale(cost) {
|
|
16
|
-
const capacity = capacityFrom(cost.groups, cost.triangles);
|
|
17
|
-
if (!capacity) {
|
|
18
|
-
return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' };
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
key: 'scale',
|
|
22
|
-
// 한 도면 목표치(500)를 채우면 1 이다. 그보다 많이 놓을 수 있어도 1 을 넘기지 않는다 —
|
|
23
|
-
// 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.
|
|
24
|
-
ratio: Math.min(1, capacity.instances / BOARD_ASSETS),
|
|
25
|
-
reading: String(capacity.instances),
|
|
26
|
-
because: 'figure.text.maturity-scale-because'
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* 한 세트로 보이나. 기준선 3·4.
|
|
31
|
-
*
|
|
32
|
-
* 셋을 본다. 팔레트 밖 표면이 없나(텍스처 0 — 확대해도 안 깨진다), 분할 수가
|
|
33
|
-
* 프리셋인가, 투명 재질을 아끼나. 셋 다 이미 형식이 위반으로 세고 있는 것들이다.
|
|
34
|
-
*/
|
|
35
|
-
function consistency(source, cost) {
|
|
36
|
-
const offPreset = source.parts.filter(part => part.segments !== undefined && !SEGMENT_PRESETS.includes(part.segments)).length;
|
|
37
|
-
const faults = (cost.surfaceTextures > 0 ? 1 : 0) +
|
|
38
|
-
(offPreset > 0 ? 1 : 0) +
|
|
39
|
-
(cost.distinctMaterials > LIMITS.materialGroups ? 1 : 0);
|
|
40
|
-
return {
|
|
41
|
-
key: 'consistency',
|
|
42
|
-
ratio: (3 - faults) / 3,
|
|
43
|
-
readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',
|
|
44
|
-
because: 'figure.text.maturity-consistency-because'
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* 되쓰나 — 같은 형상을 몇 번 다시 쓰나.
|
|
49
|
-
*
|
|
50
|
-
* `scoreOf` 가 이미 재고 있으나 **점수에는 넣지 않는다**고 선언돼 있다(한도가 아니라
|
|
51
|
-
* 덤이다). 축으로는 뜻이 있다 — 되쓰기가 많은 형상이 잘 짜인 형상이다. 다만 부품마다
|
|
52
|
-
* 형상이 달라도 잘못이 아니므로 **낮다고 나무라지 않는다.**
|
|
53
|
-
*/
|
|
54
|
-
function reuse(score) {
|
|
55
|
-
const { distinct, parts, saved } = score.reuse;
|
|
56
|
-
if (parts === 0) {
|
|
57
|
-
return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' };
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
key: 'reuse',
|
|
61
|
-
// ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다
|
|
62
|
-
ratio: 1 - distinct / parts,
|
|
63
|
-
reading: String(saved),
|
|
64
|
-
because: 'figure.text.maturity-reuse-because'
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* 정밀도 — 밝힌 등급에 걸맞게 만들어졌나.
|
|
69
|
-
*
|
|
70
|
-
* 등급(S·M·L)은 「어느 거리에서 읽히도록 만든 것인가」를 밝히는 것이다. 그런데 등급만
|
|
71
|
-
* 올려 놓고 부품 셋으로 끝내면 그 약속을 안 지킨 것이다. **한도를 얼마나 썼나**로 본다.
|
|
72
|
-
*
|
|
73
|
-
* 이 축은 「많이 쓸수록 좋다」가 아니다. 등급에 맞게 채웠나를 본다 — 그래서 넘치면
|
|
74
|
-
* 다시 내려간다.
|
|
75
|
-
*/
|
|
76
|
-
function detail(source, cost) {
|
|
77
|
-
const level = (source.detailLevel ?? 'M');
|
|
78
|
-
const limit = PART_LIMIT[level];
|
|
79
|
-
const used = cost.parts / limit;
|
|
80
|
-
return {
|
|
81
|
-
key: 'detail',
|
|
82
|
-
// 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.
|
|
83
|
-
ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),
|
|
84
|
-
reading: `${cost.parts} / ${limit}`,
|
|
85
|
-
because: 'figure.text.maturity-detail-because'
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* 살아 있나 — 기준선 2·5. 상태가 색으로 구분되고 라벨이 읽히나.
|
|
90
|
-
*
|
|
91
|
-
* **지금 잴 수 없다.** 형식에 `slot` 이 있지만 그리는 쪽이 읽지 않고(선언 한 줄이
|
|
92
|
-
* 전부다), 상태를 부품에 잇는 길 자체가 아직 정해지지 않았다(figure-model#2).
|
|
93
|
-
*
|
|
94
|
-
* 0 점으로 두지 않는다. 0 은 「이 형상이 상태를 안 보여 준다」는 뜻이 되는데, 사실은
|
|
95
|
-
* **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.
|
|
96
|
-
*/
|
|
97
|
-
function reactive(source) {
|
|
98
|
-
return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' };
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* 식별성 — 기준선 1. 위에서 볼 때 무엇인지 알아보나.
|
|
102
|
-
*
|
|
103
|
-
* ## 시점 엔트로피를 빌린다
|
|
104
|
-
*
|
|
105
|
-
* 「어느 시점에서 얼마나 알아볼 수 있나」는 이미 연구된 물음이다. Vázquez 등의
|
|
106
|
-
* **viewpoint entropy** 는 시점에서 보이는 각 면의 투영 면적 비율로 섀넌 엔트로피를
|
|
107
|
-
* 낸다. 모든 면이 같은 면적으로 보일 때가 최대이고, 한 면이 화면을 다 덮으면 0 이다.
|
|
108
|
-
*
|
|
109
|
-
* H = -Σ (Ai / At) · log(Ai / At)
|
|
110
|
-
*
|
|
111
|
-
* ## 다만 면으로 세지 않는다
|
|
112
|
-
*
|
|
113
|
-
* 그 문헌이 스스로 밝히는 한계가 **폴리곤 분할에 좌우된다**는 것이다 — 같은 형상도
|
|
114
|
-
* 잘게 쪼개면 엔트로피가 올라간다. 우리에게는 그 한계가 치명적이다. 분할 수를 8 에서
|
|
115
|
-
* 16 으로 올리는 것은 형상을 알아보기 쉽게 만드는 일이 아닌데, 면으로 세면 점수가
|
|
116
|
-
* 오른다.
|
|
117
|
-
*
|
|
118
|
-
* 그래서 **면이 아니라 색 구역으로 센다.** 눈이 구별하는 것은 삼각형이 아니라 색이
|
|
119
|
-
* 다른 덩어리다. 같은 토큰을 쓰는 부품 둘이 붙어 있으면 눈에는 한 덩어리다.
|
|
120
|
-
* 이렇게 하면 분할 수에 좌우되지 않으면서 원래 물음에 답한다.
|
|
121
|
-
*
|
|
122
|
-
* ## 기준값도 지어내지 않는다
|
|
123
|
-
*
|
|
124
|
-
* 정규화 분모를 `log(재질 group 한도 + 1)` 로 둔다. 이 형식이 허용하는 재질 group 이
|
|
125
|
-
* 셋이고 거기에 배경을 더하면 넷이므로, **이 형식에서 도달 가능한 최대**가 곧
|
|
126
|
-
* 기준이다. 임의로 고른 수가 아니다.
|
|
127
|
-
*
|
|
128
|
-
* ## 그리지 않고 셈으로 낸다
|
|
129
|
-
*
|
|
130
|
-
* 저작 중에 매번 렌더링할 수 없다. 대신 부품의 발자국(x·y·너비·높이)을 위에서
|
|
131
|
-
* 겹쳐 본다 — 위에 있는 것이 아래를 가린다. 축에 정렬된 상자이므로 격자에 찍어
|
|
132
|
-
* 세면 된다.
|
|
133
|
-
*
|
|
134
|
-
* 이 근사가 실제 렌더링과 맞는지는 하네스가 따로 확인한다
|
|
135
|
-
* (`test/browser/check-silhouette.mjs`).
|
|
136
|
-
*/
|
|
137
|
-
function readable(source, cost) {
|
|
138
|
-
const regions = topViewRegions(source);
|
|
139
|
-
if (regions.total === 0) {
|
|
140
|
-
return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' };
|
|
141
|
-
}
|
|
142
|
-
// 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.
|
|
143
|
-
// Vázquez 의 「열린 장면」 형태와 같다(최대가 log(면 수 + 1)).
|
|
144
|
-
const shares = [...regions.byMaterial.values(), regions.background]
|
|
145
|
-
.map(area => area / regions.footprint)
|
|
146
|
-
.filter(share => share > 0);
|
|
147
|
-
const entropy = -shares.reduce((sum, p) => sum + p * Math.log(p), 0);
|
|
148
|
-
const ceiling = Math.log(LIMITS.materialGroups + 1);
|
|
149
|
-
return {
|
|
150
|
-
key: 'readable',
|
|
151
|
-
ratio: Math.min(1, entropy / ceiling),
|
|
152
|
-
reading: `${regions.byMaterial.size}`,
|
|
153
|
-
because: 'figure.text.maturity-readable-because'
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* 위에서 내려다볼 때 어느 재질이 얼마나 보이나.
|
|
158
|
-
*
|
|
159
|
-
* 부품 발자국을 격자에 찍되 **높은 것이 이긴다.** 축에 정렬된 상자만 다루므로
|
|
160
|
-
* 이것으로 충분하다 — 회전한 부품은 발자국을 크게 잡아 보수적으로 본다.
|
|
161
|
-
*/
|
|
162
|
-
function topViewRegions(source) {
|
|
163
|
-
const GRID = 64;
|
|
164
|
-
const cells = new Array(GRID * GRID).fill(null);
|
|
165
|
-
const height = new Float32Array(GRID * GRID);
|
|
166
|
-
// 톱뷰의 두 축은 x 와 z 다. y 는 높이라 위에서 보면 보이지 않는다.
|
|
167
|
-
// 원점은 기준 상자의 중심이므로 격자의 가운데가 0 이다.
|
|
168
|
-
const sx = GRID / source.base.x;
|
|
169
|
-
const sz = GRID / source.base.z;
|
|
170
|
-
for (const part of source.parts) {
|
|
171
|
-
const { position, size } = part.transform;
|
|
172
|
-
const top = position.y + size.y / 2;
|
|
173
|
-
const key = materialKeyOf(part);
|
|
174
|
-
const x0 = Math.max(0, Math.floor((position.x - size.x / 2 + source.base.x / 2) * sx));
|
|
175
|
-
const x1 = Math.min(GRID, Math.ceil((position.x + size.x / 2 + source.base.x / 2) * sx));
|
|
176
|
-
const y0 = Math.max(0, Math.floor((position.z - size.z / 2 + source.base.z / 2) * sz));
|
|
177
|
-
const y1 = Math.min(GRID, Math.ceil((position.z + size.z / 2 + source.base.z / 2) * sz));
|
|
178
|
-
for (let y = y0; y < y1; y++) {
|
|
179
|
-
for (let x = x0; x < x1; x++) {
|
|
180
|
-
// 원기둥·구는 발자국 상자를 다 채우지 않는다. 모서리를 깎아 근사한다.
|
|
181
|
-
if (ROUND_FOOTPRINT.includes(part.primitive)) {
|
|
182
|
-
const cxp = (x + 0.5 - (x0 + x1) / 2) / ((x1 - x0) / 2 || 1);
|
|
183
|
-
const cyp = (y + 0.5 - (y0 + y1) / 2) / ((y1 - y0) / 2 || 1);
|
|
184
|
-
if (cxp * cxp + cyp * cyp > 1)
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const i = y * GRID + x;
|
|
188
|
-
if (cells[i] === null || top > height[i]) {
|
|
189
|
-
cells[i] = key;
|
|
190
|
-
height[i] = top;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const byMaterial = new Map();
|
|
196
|
-
let covered = 0;
|
|
197
|
-
for (const key of cells) {
|
|
198
|
-
if (key === null)
|
|
199
|
-
continue;
|
|
200
|
-
covered++;
|
|
201
|
-
byMaterial.set(key, (byMaterial.get(key) ?? 0) + 1);
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
byMaterial,
|
|
205
|
-
total: covered,
|
|
206
|
-
background: GRID * GRID - covered,
|
|
207
|
-
footprint: GRID * GRID
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
/** 위에서 보면 발자국 상자를 다 채우지 않는 도형. */
|
|
211
|
-
const ROUND_FOOTPRINT = ['cylinder', 'sphere'];
|
|
212
|
-
//# sourceMappingURL=maturity.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../client/modeller/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["import { LIMITS, PART_LIMIT, SEGMENT_PRESETS, materialKeyOf } from '@hatiolab/figure-model'\nimport type { DetailLevel, FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'\n\nimport { capacityFrom } from './scene-budget.js'\n\n/**\n * 성숙도 — 이 형상이 얼마나 잘 만들어졌나.\n *\n * ## 축이 여럿이어야 하는 이유\n *\n * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리\n * 상자도 그 축에서는 만점이다. 잘 만들어졌다는 것은 **여러 물음에 각각 답하는 것**이고,\n * 그것을 한 숫자로 뭉개면 무엇을 고쳐야 할지 알 수 없다.\n *\n * ## 축을 지어내지 않았다\n *\n * `docs/design/component-authoring-tool.md` 의 「인정받을 수 있는 최소 품질 — 기준선」\n * 다섯이 이미 있다. 그것을 잴 수 있는 형태로 옮기고, 이미 셈이 있는 것(규모·되쓰기)을\n * 더했다.\n *\n * 기준선 1 실루엣으로 알아본다 → readable **아직 측정하지 못한다**\n * 기준선 2 상태가 색으로 구분된다 → reactive **아직 측정하지 못한다**\n * 기준선 3 다른 자산과 한 세트로 보인다 → consistency\n * 기준선 4 확대해도 안 깨진다 → consistency (텍스처 0)\n * 기준선 5 라벨이 읽힌다 → reactive\n *\n * ## 못 재는 축을 지어내지 않는다\n *\n * 두 축은 지금 잴 수 없다. 하나는 그리기가 필요하고(실루엣), 하나는 형식에 길이\n * 없다(상태 바인딩 — figure-model#2). **그것을 0점으로도 만점으로도 두지 않는다.**\n * 「아직 측정하지 못한다」고 알리고 이유를 붙인다. 측정하지 못한 것을 점수로 만들면 그 점수가 거짓말을\n * 한다.\n *\n * ## 이 파일은 임시다\n *\n * 셈은 figure-model 것이어야 한다 — 거기가 `costOf` · `scoreOf` 를 갖고 있다.\n * 형식 쪽 결정(#2 · #4)이 서면 이 파일을 지우고 그쪽으로 옮긴다.\n */\n\nexport type AxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable'\n\nexport interface MaturityAxis {\n key: AxisKey\n /** 0~1. 못 재면 없다. */\n ratio?: number\n /** 지금 이 축의 값. 그대로 보여 준다. */\n reading?: string\n /** 값이 숫자가 아니라 말일 때의 문구 키. `reading` 과 함께 쓰지 않는다. */\n readingKey?: string\n /** 왜 이 값인가. */\n because?: string\n /** 못 재면 그 이유. 있으면 `ratio` 가 없다. */\n blocked?: string\n}\n\n/** 한 도면에 이만큼 놓는 것을 본다. 설계 문서 §4.15 의 「자산 500 개」. */\nconst BOARD_ASSETS = 500\n\n/**\n * 여섯 축을 측정한다.\n *\n * 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다 — 그것이 이\n * 리포트의 존재 이유다.\n */\nexport function maturityOf(source: FigureSource, cost: FigureCost, score: FigureScore): MaturityAxis[] {\n return [scale(cost), consistency(source, cost), reuse(score), detail(source, cost), reactive(source), readable(source, cost)]\n}\n\n/** 규모 — 한 도면에 얼마나 놓을 수 있나. */\nfunction scale(cost: FigureCost): MaturityAxis {\n const capacity = capacityFrom(cost.groups, cost.triangles)\n if (!capacity) {\n return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' }\n }\n\n return {\n key: 'scale',\n // 한 도면 목표치(500)를 채우면 1 이다. 그보다 많이 놓을 수 있어도 1 을 넘기지 않는다 —\n // 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.\n ratio: Math.min(1, capacity.instances / BOARD_ASSETS),\n reading: String(capacity.instances),\n because: 'figure.text.maturity-scale-because'\n }\n}\n\n/**\n * 한 세트로 보이나. 기준선 3·4.\n *\n * 셋을 본다. 팔레트 밖 표면이 없나(텍스처 0 — 확대해도 안 깨진다), 분할 수가\n * 프리셋인가, 투명 재질을 아끼나. 셋 다 이미 형식이 위반으로 세고 있는 것들이다.\n */\nfunction consistency(source: FigureSource, cost: FigureCost): MaturityAxis {\n const offPreset = source.parts.filter(\n part => part.segments !== undefined && !SEGMENT_PRESETS.includes(part.segments)\n ).length\n\n const faults =\n (cost.surfaceTextures > 0 ? 1 : 0) +\n (offPreset > 0 ? 1 : 0) +\n (cost.distinctMaterials > LIMITS.materialGroups ? 1 : 0)\n\n return {\n key: 'consistency',\n ratio: (3 - faults) / 3,\n readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',\n because: 'figure.text.maturity-consistency-because'\n }\n}\n\n/**\n * 되쓰나 — 같은 형상을 몇 번 다시 쓰나.\n *\n * `scoreOf` 가 이미 재고 있으나 **점수에는 넣지 않는다**고 선언돼 있다(한도가 아니라\n * 덤이다). 축으로는 뜻이 있다 — 되쓰기가 많은 형상이 잘 짜인 형상이다. 다만 부품마다\n * 형상이 달라도 잘못이 아니므로 **낮다고 나무라지 않는다.**\n */\nfunction reuse(score: FigureScore): MaturityAxis {\n const { distinct, parts, saved } = score.reuse\n if (parts === 0) {\n return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' }\n }\n\n return {\n key: 'reuse',\n // ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다\n ratio: 1 - distinct / parts,\n reading: String(saved),\n because: 'figure.text.maturity-reuse-because'\n }\n}\n\n/**\n * 정밀도 — 밝힌 등급에 걸맞게 만들어졌나.\n *\n * 등급(S·M·L)은 「어느 거리에서 읽히도록 만든 것인가」를 밝히는 것이다. 그런데 등급만\n * 올려 놓고 부품 셋으로 끝내면 그 약속을 안 지킨 것이다. **한도를 얼마나 썼나**로 본다.\n *\n * 이 축은 「많이 쓸수록 좋다」가 아니다. 등급에 맞게 채웠나를 본다 — 그래서 넘치면\n * 다시 내려간다.\n */\nfunction detail(source: FigureSource, cost: FigureCost): MaturityAxis {\n const level = (source.detailLevel ?? 'M') as DetailLevel\n const limit = PART_LIMIT[level]\n const used = cost.parts / limit\n\n return {\n key: 'detail',\n // 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.\n ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),\n reading: `${cost.parts} / ${limit}`,\n because: 'figure.text.maturity-detail-because'\n }\n}\n\n/**\n * 살아 있나 — 기준선 2·5. 상태가 색으로 구분되고 라벨이 읽히나.\n *\n * **지금 잴 수 없다.** 형식에 `slot` 이 있지만 그리는 쪽이 읽지 않고(선언 한 줄이\n * 전부다), 상태를 부품에 잇는 길 자체가 아직 정해지지 않았다(figure-model#2).\n *\n * 0 점으로 두지 않는다. 0 은 「이 형상이 상태를 안 보여 준다」는 뜻이 되는데, 사실은\n * **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.\n */\nfunction reactive(source: FigureSource): MaturityAxis {\n return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' }\n}\n\n/**\n * 식별성 — 기준선 1. 위에서 볼 때 무엇인지 알아보나.\n *\n * ## 시점 엔트로피를 빌린다\n *\n * 「어느 시점에서 얼마나 알아볼 수 있나」는 이미 연구된 물음이다. Vázquez 등의\n * **viewpoint entropy** 는 시점에서 보이는 각 면의 투영 면적 비율로 섀넌 엔트로피를\n * 낸다. 모든 면이 같은 면적으로 보일 때가 최대이고, 한 면이 화면을 다 덮으면 0 이다.\n *\n * H = -Σ (Ai / At) · log(Ai / At)\n *\n * ## 다만 면으로 세지 않는다\n *\n * 그 문헌이 스스로 밝히는 한계가 **폴리곤 분할에 좌우된다**는 것이다 — 같은 형상도\n * 잘게 쪼개면 엔트로피가 올라간다. 우리에게는 그 한계가 치명적이다. 분할 수를 8 에서\n * 16 으로 올리는 것은 형상을 알아보기 쉽게 만드는 일이 아닌데, 면으로 세면 점수가\n * 오른다.\n *\n * 그래서 **면이 아니라 색 구역으로 센다.** 눈이 구별하는 것은 삼각형이 아니라 색이\n * 다른 덩어리다. 같은 토큰을 쓰는 부품 둘이 붙어 있으면 눈에는 한 덩어리다.\n * 이렇게 하면 분할 수에 좌우되지 않으면서 원래 물음에 답한다.\n *\n * ## 기준값도 지어내지 않는다\n *\n * 정규화 분모를 `log(재질 group 한도 + 1)` 로 둔다. 이 형식이 허용하는 재질 group 이\n * 셋이고 거기에 배경을 더하면 넷이므로, **이 형식에서 도달 가능한 최대**가 곧\n * 기준이다. 임의로 고른 수가 아니다.\n *\n * ## 그리지 않고 셈으로 낸다\n *\n * 저작 중에 매번 렌더링할 수 없다. 대신 부품의 발자국(x·y·너비·높이)을 위에서\n * 겹쳐 본다 — 위에 있는 것이 아래를 가린다. 축에 정렬된 상자이므로 격자에 찍어\n * 세면 된다.\n *\n * 이 근사가 실제 렌더링과 맞는지는 하네스가 따로 확인한다\n * (`test/browser/check-silhouette.mjs`).\n */\nfunction readable(source: FigureSource, cost: FigureCost): MaturityAxis {\n const regions = topViewRegions(source)\n if (regions.total === 0) {\n return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }\n }\n\n // 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.\n // Vázquez 의 「열린 장면」 형태와 같다(최대가 log(면 수 + 1)).\n const shares = [...regions.byMaterial.values(), regions.background]\n .map(area => area / regions.footprint)\n .filter(share => share > 0)\n\n const entropy = -shares.reduce((sum, p) => sum + p * Math.log(p), 0)\n const ceiling = Math.log(LIMITS.materialGroups + 1)\n\n return {\n key: 'readable',\n ratio: Math.min(1, entropy / ceiling),\n reading: `${regions.byMaterial.size}`,\n because: 'figure.text.maturity-readable-because'\n }\n}\n\n/**\n * 위에서 내려다볼 때 어느 재질이 얼마나 보이나.\n *\n * 부품 발자국을 격자에 찍되 **높은 것이 이긴다.** 축에 정렬된 상자만 다루므로\n * 이것으로 충분하다 — 회전한 부품은 발자국을 크게 잡아 보수적으로 본다.\n */\nfunction topViewRegions(source: FigureSource) {\n const GRID = 64\n const cells = new Array<string | null>(GRID * GRID).fill(null)\n const height = new Float32Array(GRID * GRID)\n\n // 톱뷰의 두 축은 x 와 z 다. y 는 높이라 위에서 보면 보이지 않는다.\n // 원점은 기준 상자의 중심이므로 격자의 가운데가 0 이다.\n const sx = GRID / source.base.x\n const sz = GRID / source.base.z\n\n for (const part of source.parts) {\n const { position, size } = part.transform\n const top = position.y + size.y / 2\n const key = materialKeyOf(part)\n\n const x0 = Math.max(0, Math.floor((position.x - size.x / 2 + source.base.x / 2) * sx))\n const x1 = Math.min(GRID, Math.ceil((position.x + size.x / 2 + source.base.x / 2) * sx))\n const y0 = Math.max(0, Math.floor((position.z - size.z / 2 + source.base.z / 2) * sz))\n const y1 = Math.min(GRID, Math.ceil((position.z + size.z / 2 + source.base.z / 2) * sz))\n\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n // 원기둥·구는 발자국 상자를 다 채우지 않는다. 모서리를 깎아 근사한다.\n if (ROUND_FOOTPRINT.includes(part.primitive)) {\n const cxp = (x + 0.5 - (x0 + x1) / 2) / ((x1 - x0) / 2 || 1)\n const cyp = (y + 0.5 - (y0 + y1) / 2) / ((y1 - y0) / 2 || 1)\n if (cxp * cxp + cyp * cyp > 1) continue\n }\n const i = y * GRID + x\n if (cells[i] === null || top > height[i]) {\n cells[i] = key\n height[i] = top\n }\n }\n }\n }\n\n const byMaterial = new Map<string, number>()\n let covered = 0\n for (const key of cells) {\n if (key === null) continue\n covered++\n byMaterial.set(key, (byMaterial.get(key) ?? 0) + 1)\n }\n\n return {\n byMaterial,\n total: covered,\n background: GRID * GRID - covered,\n footprint: GRID * GRID\n }\n}\n\n/** 위에서 보면 발자국 상자를 다 채우지 않는 도형. */\nconst ROUND_FOOTPRINT: string[] = ['cylinder', 'sphere']\n"]}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** A route entry starts a new document; ordinary updates must preserve edits. */
|
|
2
|
-
export function shouldStartNewFigure(changes, hasFigure, savedId) {
|
|
3
|
-
return !hasFigure || !!savedId || changes.active === true || Object.prototype.hasOwnProperty.call(changes, 'resourceId');
|
|
4
|
-
}
|
|
5
|
-
//# sourceMappingURL=new-figure-entry.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"new-figure-entry.js","sourceRoot":"","sources":["../../client/modeller/new-figure-entry.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,UAAU,oBAAoB,CAAC,OAAgC,EAAE,SAAkB,EAAE,OAAgB;IACzG,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;AAC1H,CAAC","sourcesContent":["/** A route entry starts a new document; ordinary updates must preserve edits. */\nexport function shouldStartNewFigure(changes: Record<string, unknown>, hasFigure: boolean, savedId?: string): boolean {\n return !hasFigure || !!savedId || changes.active === true || Object.prototype.hasOwnProperty.call(changes, 'resourceId')\n}\n"]}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { FigureDraft } from './figure-source.js';
|
|
2
|
-
/** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */
|
|
3
|
-
export declare function poseChanges(draft: FigureDraft | undefined, values: Record<string, number>): {
|
|
4
|
-
name: string;
|
|
5
|
-
from: number;
|
|
6
|
-
to: number;
|
|
7
|
-
}[];
|
|
8
|
-
/** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */
|
|
9
|
-
export declare function poseAsDefaults(draft: FigureDraft, values: Record<string, number>): FigureDraft;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
-
*/
|
|
4
|
-
/** 지금 이 파라미터가 서는 값. 형식의 `valueOf` 와 같은 순서로 읽는다. */
|
|
5
|
-
function standing(parameter) {
|
|
6
|
-
return parameter.default ?? parameter.range?.min ?? 0;
|
|
7
|
-
}
|
|
8
|
-
/** 범위 밖은 끝에서 문다. 막대는 범위 안에서만 움직이지만, 값은 다른 길로도 들어온다. */
|
|
9
|
-
function settled(value, parameter) {
|
|
10
|
-
const range = parameter.range;
|
|
11
|
-
const bound = range ? Math.min(Math.max(value, range.min), range.max) : value;
|
|
12
|
-
return Math.round(bound * 10000) / 10000;
|
|
13
|
-
}
|
|
14
|
-
/** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */
|
|
15
|
-
export function poseChanges(draft, values) {
|
|
16
|
-
return (draft?.parameters ?? []).flatMap(parameter => {
|
|
17
|
-
const given = values[parameter.name];
|
|
18
|
-
if (given === undefined || !Number.isFinite(given))
|
|
19
|
-
return [];
|
|
20
|
-
const to = settled(given, parameter);
|
|
21
|
-
const from = standing(parameter);
|
|
22
|
-
return to === from ? [] : [{ name: parameter.name, from, to }];
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
/** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */
|
|
26
|
-
export function poseAsDefaults(draft, values) {
|
|
27
|
-
const changes = new Map(poseChanges(draft, values).map(change => [change.name, change.to]));
|
|
28
|
-
if (changes.size === 0)
|
|
29
|
-
return draft;
|
|
30
|
-
return {
|
|
31
|
-
...draft,
|
|
32
|
-
parameters: (draft.parameters ?? []).map(parameter => changes.has(parameter.name) ? { ...parameter, default: changes.get(parameter.name) } : parameter)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
//# sourceMappingURL=pose-defaults.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pose-defaults.js","sourceRoot":"","sources":["../../client/modeller/pose-defaults.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,mDAAmD;AACnD,SAAS,QAAQ,CAAC,SAAoB;IACpC,OAAO,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAA;AACvD,CAAC;AAED,uDAAuD;AACvD,SAAS,OAAO,CAAC,KAAa,EAAE,SAAoB;IAClD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAA;IAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAC7E,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAA;AAC1C,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,WAAW,CACzB,KAA8B,EAC9B,MAA8B;IAE9B,OAAO,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAA;QAE7D,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;QACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAA;QAChC,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,KAAkB,EAAE,MAA8B;IAC/E,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC3F,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEpC,OAAO;QACL,GAAG,KAAK;QACR,UAAU,EAAE,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CACnD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAClG;KACF,CAAA;AACH,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureDraft } from './figure-source.js'\n\n/**\n * 지금 세워 둔 자세를 **기본값으로 앉힌다.**\n *\n * ## 왜 필요한가\n *\n * 관절의 휴지 자세는 값이 0 인 자세다(ADR-0066). 팔이 여섯 마디면 그 자세는 곧게 선 기둥이라,\n * 목록에서도 도면에서도 로봇으로 안 보인다. 로봇다운 자세는 **파라미터의 기본값**이 만든다 —\n * 어깨 -35, 팔꿈치 70 을 적어 두면 아무도 값을 주지 않을 때 그 자세로 선다.\n *\n * 그것을 손으로 적으려면 미리보기에서 막대를 맞춘 뒤 그 수를 읽어 파라미터 칸마다 옮겨야 한다.\n * 여섯 축이면 여섯 번이고, 옮기는 동안 화면의 자세와 적은 수가 갈린다. 여기서 하는 일은 그\n * 옮겨 적기 하나다.\n *\n * ## 무엇을 건드리나\n *\n * **막대를 민 파라미터만** 건드린다. 안 민 것은 이미 제 기본값에 서 있으므로 적을 것이 없다.\n * 값이 지금 기본값과 같으면 그 파라미터는 **그대로 둔다** — 새 객체로 바꾸면 고친 것이 없는데도\n * 「저장 안 됨」이 붙는다. 하나도 안 바뀌면 받은 초안을 그대로 돌려준다.\n */\n\ntype Parameter = NonNullable<FigureDraft['parameters']>[number]\n\n/** 지금 이 파라미터가 서는 값. 형식의 `valueOf` 와 같은 순서로 읽는다. */\nfunction standing(parameter: Parameter): number {\n return parameter.default ?? parameter.range?.min ?? 0\n}\n\n/** 범위 밖은 끝에서 문다. 막대는 범위 안에서만 움직이지만, 값은 다른 길로도 들어온다. */\nfunction settled(value: number, parameter: Parameter): number {\n const range = parameter.range\n const bound = range ? Math.min(Math.max(value, range.min), range.max) : value\n return Math.round(bound * 10000) / 10000\n}\n\n/** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */\nexport function poseChanges(\n draft: FigureDraft | undefined,\n values: Record<string, number>\n): { name: string; from: number; to: number }[] {\n return (draft?.parameters ?? []).flatMap(parameter => {\n const given = values[parameter.name]\n if (given === undefined || !Number.isFinite(given)) return []\n\n const to = settled(given, parameter)\n const from = standing(parameter)\n return to === from ? [] : [{ name: parameter.name, from, to }]\n })\n}\n\n/** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */\nexport function poseAsDefaults(draft: FigureDraft, values: Record<string, number>): FigureDraft {\n const changes = new Map(poseChanges(draft, values).map(change => [change.name, change.to]))\n if (changes.size === 0) return draft\n\n return {\n ...draft,\n parameters: (draft.parameters ?? []).map(parameter =>\n changes.has(parameter.name) ? { ...parameter, default: changes.get(parameter.name)! } : parameter\n )\n }\n}\n"]}
|