@things-factory/figure-ui 10.1.16 → 10.1.17
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-animations.ts +12 -12
- package/client/modeller/figure-canvas.ts +32 -48
- package/client/modeller/figure-capabilities.ts +10 -10
- package/client/modeller/figure-inspector.ts +39 -28
- package/client/modeller/figure-parts.ts +1 -1
- package/client/modeller/figure-preview.ts +122 -25
- package/client/modeller/figure-report.ts +24 -6
- package/client/modeller/figure-settings.ts +45 -2
- package/client/modeller/figure-side.ts +6 -6
- package/client/modeller/figure-source.ts +52 -32
- package/client/modeller/figure-thumbnail.ts +5 -5
- package/client/modeller/figure-view.ts +53 -0
- package/client/modeller/part-edits.ts +36 -36
- package/client/modeller/proposal.ts +25 -4
- package/client/pages/figure-modeller-page.ts +64 -58
- package/dist-client/modeller/figure-animations.d.ts +4 -4
- package/dist-client/modeller/figure-animations.js +10 -10
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +7 -7
- package/dist-client/modeller/figure-canvas.js +27 -40
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +2 -2
- package/dist-client/modeller/figure-capabilities.js +9 -9
- package/dist-client/modeller/figure-capabilities.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +16 -5
- package/dist-client/modeller/figure-inspector.js +38 -27
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +1 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +39 -0
- package/dist-client/modeller/figure-preview.js +112 -23
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +9 -0
- package/dist-client/modeller/figure-report.js +23 -5
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +42 -2
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +2 -2
- package/dist-client/modeller/figure-side.js +5 -5
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +17 -15
- package/dist-client/modeller/figure-source.js +40 -21
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
- package/dist-client/modeller/figure-thumbnail.js +3 -3
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +45 -0
- package/dist-client/modeller/figure-view.js +27 -0
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/part-edits.d.ts +16 -16
- package/dist-client/modeller/part-edits.js +35 -35
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +15 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +9 -10
- package/dist-client/pages/figure-modeller-page.js +60 -54
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +393 -12
- package/test/expect.ts +55 -0
- package/test/figure-source.test.ts +562 -0
- package/test/i18n-prefix-guard.test.ts +1 -1
- package/{client/modeller → test}/part-edits.test.ts +50 -45
- package/translations/en.json +31 -28
- package/translations/ja.json +16 -21
- package/translations/ko.json +41 -38
- package/translations/ms.json +16 -21
- package/translations/zh.json +16 -21
- package/client/modeller/figure-ask.ts +0 -265
- package/client/modeller/figure-source.test.ts +0 -352
- package/dist-client/modeller/figure-ask.d.ts +0 -40
- package/dist-client/modeller/figure-ask.js +0 -282
- package/dist-client/modeller/figure-ask.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model';
|
|
2
|
-
import { AXIS_KEYS,
|
|
2
|
+
import { AXIS_KEYS, draftExtent } from './figure-source.js';
|
|
3
3
|
/**
|
|
4
4
|
* 부품 편집 규칙. **씬 모델의 용어로** 다룬다.
|
|
5
5
|
*
|
|
6
|
-
* 편집 중에 기준이 되는 데이터는 씬 모델이다 —
|
|
6
|
+
* 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.
|
|
7
7
|
* 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.
|
|
8
8
|
* FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.
|
|
9
9
|
*
|
|
@@ -23,30 +23,30 @@ function freeName(taken, base) {
|
|
|
23
23
|
return candidate;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
/** 이
|
|
27
|
-
export function partLimit(
|
|
28
|
-
return PART_LIMIT[(
|
|
26
|
+
/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
|
|
27
|
+
export function partLimit(draft) {
|
|
28
|
+
return PART_LIMIT[(draft.detailLevel ?? 'M')];
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* 새 부품 하나를 만든다.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
33
|
+
* 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —
|
|
34
34
|
* 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
|
|
35
35
|
*/
|
|
36
|
-
export function newPart(
|
|
36
|
+
export function newPart(draft, parts, primitive) {
|
|
37
37
|
const taken = new Set(parts.map(part => part.name));
|
|
38
|
-
const width = Math.max(1, Math.round(
|
|
39
|
-
const height = Math.max(1, Math.round(
|
|
40
|
-
const depth = Math.max(1, Math.round(
|
|
38
|
+
const width = Math.max(1, Math.round(draft.width / 3));
|
|
39
|
+
const height = Math.max(1, Math.round(draft.height / 3));
|
|
40
|
+
const depth = Math.max(1, Math.round(draft.depth / 3));
|
|
41
41
|
const part = {
|
|
42
42
|
type: 'figure-part',
|
|
43
43
|
name: freeName(taken, primitive),
|
|
44
44
|
primitive,
|
|
45
|
-
left: Math.round((
|
|
46
|
-
top: Math.round((
|
|
45
|
+
left: Math.round((draft.width - width) / 2),
|
|
46
|
+
top: Math.round((draft.height - height) / 2),
|
|
47
47
|
width,
|
|
48
48
|
height,
|
|
49
|
-
// 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧
|
|
49
|
+
// 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.
|
|
50
50
|
zPos: 0,
|
|
51
51
|
depth,
|
|
52
52
|
// 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.
|
|
@@ -69,8 +69,8 @@ export function newPart(board, parts, primitive) {
|
|
|
69
69
|
}
|
|
70
70
|
return part;
|
|
71
71
|
}
|
|
72
|
-
export function addPart(
|
|
73
|
-
return [...parts, newPart(
|
|
72
|
+
export function addPart(draft, parts, primitive) {
|
|
73
|
+
return [...parts, newPart(draft, parts, primitive)];
|
|
74
74
|
}
|
|
75
75
|
export function removePart(parts, index) {
|
|
76
76
|
return parts.filter((_, at) => at !== index);
|
|
@@ -115,23 +115,23 @@ function put(parts, index, fields) {
|
|
|
115
115
|
function putAt(parts, index, axis, value) {
|
|
116
116
|
return put(parts, index, { [AXIS_KEYS[axis].at]: value });
|
|
117
117
|
}
|
|
118
|
-
/** 부품을
|
|
119
|
-
export function centerPart(
|
|
118
|
+
/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */
|
|
119
|
+
export function centerPart(draft, parts, index, axis) {
|
|
120
120
|
const part = parts[index];
|
|
121
121
|
if (!part)
|
|
122
122
|
return parts;
|
|
123
|
-
return putAt(parts, index, axis, Math.round((
|
|
123
|
+
return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2));
|
|
124
124
|
}
|
|
125
|
-
/** 부품을
|
|
125
|
+
/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */
|
|
126
126
|
export function alignPartToStart(parts, index, axis) {
|
|
127
127
|
return putAt(parts, index, axis, 0);
|
|
128
128
|
}
|
|
129
|
-
/** 부품을
|
|
130
|
-
export function alignPartToEnd(
|
|
129
|
+
/** 부품을 상자의 끝에 붙인다. */
|
|
130
|
+
export function alignPartToEnd(draft, parts, index, axis) {
|
|
131
131
|
const part = parts[index];
|
|
132
132
|
if (!part)
|
|
133
133
|
return parts;
|
|
134
|
-
return putAt(parts, index, axis, Math.round(
|
|
134
|
+
return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]));
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
137
|
* 부품을 축 기준으로 대칭 복제한다.
|
|
@@ -139,20 +139,20 @@ export function alignPartToEnd(board, parts, index, axis) {
|
|
|
139
139
|
* 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수
|
|
140
140
|
* 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.
|
|
141
141
|
*/
|
|
142
|
-
export function mirrorPart(
|
|
142
|
+
export function mirrorPart(draft, parts, index, axis) {
|
|
143
143
|
const part = parts[index];
|
|
144
|
-
if (!part || !canMirror(
|
|
144
|
+
if (!part || !canMirror(draft, parts, index, axis))
|
|
145
145
|
return parts;
|
|
146
146
|
const copy = JSON.parse(JSON.stringify(part));
|
|
147
147
|
copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`);
|
|
148
148
|
const next = [...parts];
|
|
149
149
|
next.splice(index + 1, 0, copy);
|
|
150
|
-
return putAt(next, index + 1, axis, mirroredAt(
|
|
150
|
+
return putAt(next, index + 1, axis, mirroredAt(draft, part, axis));
|
|
151
151
|
}
|
|
152
|
-
/** 대칭했을 때의 위치.
|
|
153
|
-
function mirroredAt(
|
|
152
|
+
/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */
|
|
153
|
+
function mirroredAt(draft, part, axis) {
|
|
154
154
|
const { at, size } = AXIS_KEYS[axis];
|
|
155
|
-
return Math.round(
|
|
155
|
+
return Math.round(draftExtent(draft, axis) - part[at] - part[size]);
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
158
|
* 대칭이 의미가 있는지.
|
|
@@ -161,23 +161,23 @@ function mirroredAt(board, part, axis) {
|
|
|
161
161
|
* 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.
|
|
162
162
|
* 만들어 놓고 알리는 대신 미리 막는다.
|
|
163
163
|
*/
|
|
164
|
-
export function canMirror(
|
|
164
|
+
export function canMirror(draft, parts, index, axis) {
|
|
165
165
|
const part = parts[index];
|
|
166
166
|
if (!part)
|
|
167
167
|
return false;
|
|
168
|
-
return mirroredAt(
|
|
168
|
+
return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at]);
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
171
|
* 부품의 위치와 크기를 격자에 맞춘다.
|
|
172
172
|
*
|
|
173
|
-
* 격자 간격은
|
|
173
|
+
* 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
|
|
174
174
|
* 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.
|
|
175
175
|
*/
|
|
176
|
-
export function snapPart(
|
|
176
|
+
export function snapPart(draft, parts, index) {
|
|
177
177
|
const part = parts[index];
|
|
178
178
|
if (!part)
|
|
179
179
|
return parts;
|
|
180
|
-
const step = gridStep(
|
|
180
|
+
const step = gridStep(draft);
|
|
181
181
|
const round = (value) => Math.round(value / step) * step;
|
|
182
182
|
return put(parts, index, {
|
|
183
183
|
left: round(part.left),
|
|
@@ -196,8 +196,8 @@ export function snapPart(board, parts, index) {
|
|
|
196
196
|
* (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가
|
|
197
197
|
* 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.
|
|
198
198
|
*/
|
|
199
|
-
export function gridStep(
|
|
200
|
-
return niceStep(Math.max(
|
|
199
|
+
export function gridStep(draft) {
|
|
200
|
+
return niceStep(Math.max(draft.width, draft.height, draft.depth));
|
|
201
201
|
}
|
|
202
202
|
/**
|
|
203
203
|
* 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"part-edits.js","sourceRoot":"","sources":["../../client/modeller/part-edits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAG9G,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAG3D;;;;;;;;;;;;GAYG;AAEH,uDAAuD;AACvD,SAAS,QAAQ,CAAC,KAAkB,EAAE,IAAY;IAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACjC,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,CAAC,EAAE,CAAA;QAChC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAA;IAC7C,CAAC;AACH,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,SAAS,CAAC,KAAiB;IACzC,OAAO,UAAU,CAAC,CAAC,KAAK,CAAC,WAAW,IAAI,GAAG,CAAgB,CAAC,CAAA;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,KAAiB,EAAE,KAAkB,EAAE,SAAwB;IACrF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAEnD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;IACtD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;IAEtD,MAAM,IAAI,GAAc;QACtB,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;QAChC,SAAS;QACT,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3C,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5C,KAAK;QACL,MAAM;QACN,4CAA4C;QAC5C,IAAI,EAAE,CAAC;QACP,KAAK;QACL,uDAAuD;QACvD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,iBAAiB;KAC5C,CAAA;IAED,IAAI,oBAAoB,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,CAAA;IACpC,CAAC;IACD,IAAI,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;IAChB,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,sDAAsD;QACtD,0BAA0B;QAC1B,IAAI,CAAC,IAAI,GAAG;YACV,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE;YACzD,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE;YACxD,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE;SACpC,CAAA;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAiB,EAAE,KAAkB,EAAE,SAAwB;IACrF,OAAO,CAAC,GAAG,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAkB,EAAE,KAAa;IAC1D,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,KAAK,CAAC,CAAA;AAC9C,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,aAAa,CAAC,KAAkB,EAAE,KAAa;IAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IAC3B,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEzB,MAAM,IAAI,GAAc,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1D,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,OAAO,CAAC,CAAA;IAElF,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IACvB,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;IAC/B,OAAO,IAAI,CAAA;AACb,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,QAAQ,CAAC,KAAkB,EAAE,KAAa,EAAE,EAAU;IACpE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,KAAK,CAAA;IAE9D,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IACvB,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IACrC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,EAAE,KAAM,CAAC,CAAA;IAC1B,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAkB,EAAE,KAAa,EAAE,IAAe;IAC3E,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;;GAMG;AAEH,SAAS,GAAG,CAAC,KAAkB,EAAE,KAAa,EAAE,MAA0B;IACxE,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IACvB,OAAO,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,CAAA;AACzD,CAAC;AAED,iDAAiD;AACjD,SAAS,KAAK,CAAC,KAAkB,EAAE,KAAa,EAAE,IAAe,EAAE,KAAa;IAC9E,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAwB,CAAC,CAAA;AACjF,CAAC;AAED,6BAA6B;AAC7B,MAAM,UAAU,UAAU,CAAC,KAAiB,EAAE,KAAkB,EAAE,KAAa,EAAE,IAAe;IAC9F,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAEvB,OAAO,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3G,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,KAAa,EAAE,IAAe;IACjF,OAAO,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;AACrC,CAAC;AAED,sBAAsB;AACtB,MAAM,UAAU,cAAc,CAAC,KAAiB,EAAE,KAAkB,EAAE,KAAa,EAAE,IAAe;IAClG,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAEvB,OAAO,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAiB,EAAE,KAAkB,EAAE,KAAa,EAAE,IAAe;IAC9F,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IAEhE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAc,CAAA;IAC1D,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,CAAA;IAE7E,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IACvB,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;IAC/B,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AACpE,CAAC;AAED,qCAAqC;AACrC,SAAS,UAAU,CAAC,KAAiB,EAAE,IAAe,EAAE,IAAe;IACrE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IACpC,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;AACrE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,KAAiB,EAAE,KAAkB,EAAE,KAAa,EAAE,IAAe;IAC7F,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAEvB,OAAO,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAC/E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAiB,EAAE,KAAkB,EAAE,KAAa;IAC3E,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAEvB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC5B,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;IAEhE,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE;QACvB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACtB,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACtB,oCAAoC;QACpC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1C,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;KACzC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAiB;IACxC,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;AACnE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAC3D,MAAM,CAAC,GAAG,GAAG,GAAG,SAAS,CAAA;IAEzB,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport { AXIS_KEYS, boardExtent } from './figure-source.js'\nimport type { BoardModel, PartModel, PlaneAxis } from './figure-source.js'\n\n/**\n * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.\n *\n * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 보드 위에 부품 컴포넌트가 놓인 상태.\n * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.\n * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.\n *\n * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.\n * 여기서 다시 정의하지 않는다.\n *\n * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을\n * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.\n */\n\n/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */\nfunction freeName(taken: Set<string>, base: string): string {\n if (!taken.has(base)) return base\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}`\n if (!taken.has(candidate)) return candidate\n }\n}\n\n/** 이 보드에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */\nexport function partLimit(board: BoardModel): number {\n return PART_LIMIT[(board.detailLevel ?? 'M') as DetailLevel]\n}\n\n/**\n * 새 부품 하나를 만든다.\n *\n * 보드의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —\n * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.\n */\nexport function newPart(board: BoardModel, parts: PartModel[], primitive: PrimitiveKind): PartModel {\n const taken = new Set(parts.map(part => part.name))\n\n const width = Math.max(1, Math.round(board.width / 3))\n const height = Math.max(1, Math.round(board.height / 3))\n const depth = Math.max(1, Math.round(board.depth / 3))\n\n const part: PartModel = {\n type: 'figure-part',\n name: freeName(taken, primitive),\n primitive,\n left: Math.round((board.width - width) / 2),\n top: Math.round((board.height - height) / 2),\n width,\n height,\n // 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 보드 바닥이다.\n zPos: 0,\n depth,\n // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.\n token: parts[0]?.token ?? 'palette.primary'\n }\n\n if (SEGMENTED_PRIMITIVES.includes(primitive)) {\n part.segments = SEGMENT_PRESETS[1]\n }\n if (EXTRUDE_PRIMITIVES.includes(primitive)) {\n part.round = 0\n }\n if (primitive === 'polygon') {\n // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.\n // 단면의 점은 부품 중심을 기준으로 적는다.\n part.path = [\n { x: -Math.round(width / 2), y: -Math.round(height / 2) },\n { x: Math.round(width / 2), y: -Math.round(height / 2) },\n { x: 0, y: Math.round(height / 2) }\n ]\n }\n\n return part\n}\n\nexport function addPart(board: BoardModel, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {\n return [...parts, newPart(board, parts, primitive)]\n}\n\nexport function removePart(parts: PartModel[], index: number): PartModel[] {\n return parts.filter((_, at) => at !== index)\n}\n\n/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */\nexport function duplicatePart(parts: PartModel[], index: number): PartModel[] {\n const origin = parts[index]\n if (!origin) return parts\n\n const copy: PartModel = JSON.parse(JSON.stringify(origin))\n copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return next\n}\n\n/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */\nexport function movePart(parts: PartModel[], index: number, to: number): PartModel[] {\n if (to < 0 || to >= parts.length || index === to) return parts\n\n const next = [...parts]\n const [moved] = next.splice(index, 1)\n next.splice(to, 0, moved!)\n return next\n}\n\nexport function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {\n return parts.map((old, at) => (at === index ? part : old))\n}\n\n/**\n * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.\n *\n * 보드 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을\n * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로\n * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.\n */\n\nfunction put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n return updatePart(parts, index, { ...part, ...fields })\n}\n\n/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */\nfunction putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {\n return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)\n}\n\n/** 부품을 보드의 해당 축 가운데로 옮긴다. */\nexport function centerPart(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round((boardExtent(board, axis) - part[AXIS_KEYS[axis].size]) / 2))\n}\n\n/** 부품을 보드 바닥(또는 해당 축의 시작)에 붙인다. */\nexport function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n return putAt(parts, index, axis, 0)\n}\n\n/** 부품을 보드의 끝에 붙인다. */\nexport function alignPartToEnd(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round(boardExtent(board, axis) - part[AXIS_KEYS[axis].size]))\n}\n\n/**\n * 부품을 축 기준으로 대칭 복제한다.\n *\n * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수\n * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.\n */\nexport function mirrorPart(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part || !canMirror(board, parts, index, axis)) return parts\n\n const copy = JSON.parse(JSON.stringify(part)) as PartModel\n copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return putAt(next, index + 1, axis, mirroredAt(board, part, axis))\n}\n\n/** 대칭했을 때의 위치. 보드의 해당 축 안에서 뒤집는다. */\nfunction mirroredAt(board: BoardModel, part: PartModel, axis: PlaneAxis): number {\n const { at, size } = AXIS_KEYS[axis]\n return Math.round(boardExtent(board, axis) - part[at] - part[size])\n}\n\n/**\n * 대칭이 의미가 있는지.\n *\n * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는\n * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.\n * 만들어 놓고 알리는 대신 미리 막는다.\n */\nexport function canMirror(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): boolean {\n const part = parts[index]\n if (!part) return false\n\n return mirroredAt(board, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])\n}\n\n/**\n * 부품의 위치와 크기를 격자에 맞춘다.\n *\n * 격자 간격은 보드 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰\n * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.\n */\nexport function snapPart(board: BoardModel, parts: PartModel[], index: number): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n const step = gridStep(board)\n const round = (value: number) => Math.round(value / step) * step\n\n return put(parts, index, {\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다\n width: Math.max(step, round(part.width)),\n height: Math.max(step, round(part.height)),\n depth: Math.max(step, round(part.depth))\n })\n}\n\n/**\n * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.\n *\n * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워\n * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가\n * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.\n */\nexport function gridStep(board: BoardModel): number {\n return niceStep(Math.max(board.width, board.height, board.depth))\n}\n\n/**\n * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.\n *\n * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기\n * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.\n *\n * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이\n * 먼저 보였다.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"part-edits.js","sourceRoot":"","sources":["../../client/modeller/part-edits.ts"],"names":[],"mappings":"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","sourcesContent":["import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport { AXIS_KEYS, draftExtent } from './figure-source.js'\nimport type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'\n\n/**\n * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.\n *\n * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.\n * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.\n * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.\n *\n * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.\n * 여기서 다시 정의하지 않는다.\n *\n * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을\n * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.\n */\n\n/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */\nfunction freeName(taken: Set<string>, base: string): string {\n if (!taken.has(base)) return base\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}`\n if (!taken.has(candidate)) return candidate\n }\n}\n\n/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */\nexport function partLimit(draft: FigureDraft): number {\n return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]\n}\n\n/**\n * 새 부품 하나를 만든다.\n *\n * 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —\n * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.\n */\nexport function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel {\n const taken = new Set(parts.map(part => part.name))\n\n const width = Math.max(1, Math.round(draft.width / 3))\n const height = Math.max(1, Math.round(draft.height / 3))\n const depth = Math.max(1, Math.round(draft.depth / 3))\n\n const part: PartModel = {\n type: 'figure-part',\n name: freeName(taken, primitive),\n primitive,\n left: Math.round((draft.width - width) / 2),\n top: Math.round((draft.height - height) / 2),\n width,\n height,\n // 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.\n zPos: 0,\n depth,\n // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.\n token: parts[0]?.token ?? 'palette.primary'\n }\n\n if (SEGMENTED_PRIMITIVES.includes(primitive)) {\n part.segments = SEGMENT_PRESETS[1]\n }\n if (EXTRUDE_PRIMITIVES.includes(primitive)) {\n part.round = 0\n }\n if (primitive === 'polygon') {\n // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.\n // 단면의 점은 부품 중심을 기준으로 적는다.\n part.path = [\n { x: -Math.round(width / 2), y: -Math.round(height / 2) },\n { x: Math.round(width / 2), y: -Math.round(height / 2) },\n { x: 0, y: Math.round(height / 2) }\n ]\n }\n\n return part\n}\n\nexport function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {\n return [...parts, newPart(draft, parts, primitive)]\n}\n\nexport function removePart(parts: PartModel[], index: number): PartModel[] {\n return parts.filter((_, at) => at !== index)\n}\n\n/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */\nexport function duplicatePart(parts: PartModel[], index: number): PartModel[] {\n const origin = parts[index]\n if (!origin) return parts\n\n const copy: PartModel = JSON.parse(JSON.stringify(origin))\n copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return next\n}\n\n/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */\nexport function movePart(parts: PartModel[], index: number, to: number): PartModel[] {\n if (to < 0 || to >= parts.length || index === to) return parts\n\n const next = [...parts]\n const [moved] = next.splice(index, 1)\n next.splice(to, 0, moved!)\n return next\n}\n\nexport function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {\n return parts.map((old, at) => (at === index ? part : old))\n}\n\n/**\n * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.\n *\n * 보드 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을\n * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로\n * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.\n */\n\nfunction put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n return updatePart(parts, index, { ...part, ...fields })\n}\n\n/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */\nfunction putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {\n return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)\n}\n\n/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */\nexport function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))\n}\n\n/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */\nexport function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n return putAt(parts, index, axis, 0)\n}\n\n/** 부품을 상자의 끝에 붙인다. */\nexport function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))\n}\n\n/**\n * 부품을 축 기준으로 대칭 복제한다.\n *\n * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수\n * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.\n */\nexport function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part || !canMirror(draft, parts, index, axis)) return parts\n\n const copy = JSON.parse(JSON.stringify(part)) as PartModel\n copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))\n}\n\n/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */\nfunction mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {\n const { at, size } = AXIS_KEYS[axis]\n return Math.round(draftExtent(draft, axis) - part[at] - part[size])\n}\n\n/**\n * 대칭이 의미가 있는지.\n *\n * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는\n * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.\n * 만들어 놓고 알리는 대신 미리 막는다.\n */\nexport function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {\n const part = parts[index]\n if (!part) return false\n\n return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])\n}\n\n/**\n * 부품의 위치와 크기를 격자에 맞춘다.\n *\n * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰\n * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.\n */\nexport function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n const step = gridStep(draft)\n const round = (value: number) => Math.round(value / step) * step\n\n return put(parts, index, {\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다\n width: Math.max(step, round(part.width)),\n height: Math.max(step, round(part.height)),\n depth: Math.max(step, round(part.depth))\n })\n}\n\n/**\n * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.\n *\n * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워\n * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가\n * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.\n */\nexport function gridStep(draft: FigureDraft): number {\n return niceStep(Math.max(draft.width, draft.height, draft.depth))\n}\n\n/**\n * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.\n *\n * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기\n * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.\n *\n * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이\n * 먼저 보였다.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
@@ -19,7 +19,7 @@ import type { FigurePart, FigureSource } from '@hatiolab/figure-model';
|
|
|
19
19
|
* 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준
|
|
20
20
|
* 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.
|
|
21
21
|
*/
|
|
22
|
-
export type ChangeKind = 'added' | 'changed' | 'removed' | 'base' | '
|
|
22
|
+
export type ChangeKind = 'added' | 'changed' | 'removed' | 'base' | 'placement' | 'detail' | 'styleKit' | 'animations' | 'capabilities';
|
|
23
23
|
export interface ProposalChange {
|
|
24
24
|
kind: ChangeKind;
|
|
25
25
|
/** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */
|
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도
|
|
5
5
|
* 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.
|
|
6
|
+
*
|
|
7
|
+
* 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.
|
|
8
|
+
* 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는
|
|
9
|
+
* 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만
|
|
10
|
+
* 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)
|
|
11
|
+
* 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.
|
|
12
|
+
*
|
|
13
|
+
* 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 여섯이 전부다. `type` 은 저장되는
|
|
14
|
+
* 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이
|
|
15
|
+
* 아니라 저장할 때 서버가 찍는다.
|
|
6
16
|
*/
|
|
7
|
-
const FIGURE_FIELDS = ['base', '
|
|
17
|
+
const FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'capabilities'];
|
|
8
18
|
const FIELD_KIND = {
|
|
9
19
|
base: 'base',
|
|
20
|
+
placement: 'placement',
|
|
10
21
|
detailLevel: 'detail',
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
styleKit: 'styleKit',
|
|
23
|
+
animations: 'animations',
|
|
24
|
+
capabilities: 'capabilities'
|
|
13
25
|
};
|
|
14
26
|
/** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */
|
|
15
27
|
function samePart(a, b) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../client/modeller/proposal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../client/modeller/proposal.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigurePart, FigureSource } from '@hatiolab/figure-model'\n\n/**\n * AI 후보를 지금 것과 비교하고, 고른 것만 받아들인다.\n *\n * 비교는 **저장 형식**으로 한다. 편집 중의 데이터는 씬 모델이지만, 후보는 서버에서\n * FigureSource 로 오고 기준 치수·디테일 등급처럼 부품 밖의 값도 함께 봐야 한다.\n *\n * 부품 편집 규칙은 여기 없다 — `part-edits.ts` 가 씬 모델 용어로 갖고 있다.\n */\n\n/**\n * 후보가 무엇을 바꾸자는 것인가.\n *\n * 후보를 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 —\n * 그게 가장 흔한 반응이다. 그래서 후보를 **변경 목록**으로 본다.\n *\n * 부품은 이름으로 짝짓는다. `FigurePart.name` 이 저장되는 식별자이고 바인딩이\n * 그 이름을 가리키므로, 이름이 같으면 같은 부품이다.\n *\n * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준\n * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.\n */\nexport type ChangeKind =\n | 'added'\n | 'changed'\n | 'removed'\n | 'base'\n | 'placement'\n | 'detail'\n | 'styleKit'\n | 'animations'\n | 'capabilities'\n\n/**\n * 부품 밖에서 바뀔 수 있는 것 전부.\n *\n * 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도\n * 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.\n *\n * 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.\n * 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는\n * 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만\n * 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)\n * 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.\n *\n * 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 여섯이 전부다. `type` 은 저장되는\n * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이\n * 아니라 저장할 때 서버가 찍는다.\n */\nconst FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'capabilities'] as const\nconst FIELD_KIND: Record<(typeof FIGURE_FIELDS)[number], ChangeKind> = {\n base: 'base',\n placement: 'placement',\n detailLevel: 'detail',\n styleKit: 'styleKit',\n animations: 'animations',\n capabilities: 'capabilities'\n}\n\nexport interface ProposalChange {\n kind: ChangeKind\n /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */\n key: string\n /** 부품 변경일 때 후보 쪽 부품. */\n part?: FigurePart\n}\n\n/** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */\nfunction samePart(a: FigurePart, b: FigurePart): boolean {\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\n/** 키 순서를 맞춰 견줄 수 있게 한다. */\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as object).sort()) {\n out[key] = sortKeys((value as Record<string, unknown>)[key])\n }\n return out\n }\n return value\n}\n\nexport function diffProposal(current: FigureSource | undefined, proposal: FigureSource): ProposalChange[] {\n const changes: ProposalChange[] = []\n const mine = new Map((current?.parts ?? []).map(part => [part.name, part]))\n const theirs = new Map(proposal.parts.map(part => [part.name, part]))\n\n for (const field of FIGURE_FIELDS) {\n if (!current) break\n const before = JSON.stringify(sortKeys(current[field]))\n const after = JSON.stringify(sortKeys(proposal[field]))\n if (before !== after) {\n changes.push({ kind: FIELD_KIND[field], key: field })\n }\n }\n\n for (const part of proposal.parts) {\n const old = mine.get(part.name)\n if (!old) changes.push({ kind: 'added', key: part.name, part })\n else if (!samePart(old, part)) changes.push({ kind: 'changed', key: part.name, part })\n }\n for (const part of current?.parts ?? []) {\n if (!theirs.has(part.name)) changes.push({ kind: 'removed', key: part.name })\n }\n\n return changes\n}\n\n/**\n * 고른 변경만 받는다.\n *\n * **타입 이름은 지금 것을 지킨다.** 만든 뒤에는 고칠 수 없는 저장 식별자이고,\n * 배치된 인스턴스가 그 이름으로 청사진을 찾는다. 후보가 다른 이름을 지어 왔어도\n * 그것 때문에 이미 놓인 것이 빈 자리가 되면 안 된다.\n *\n * `picked` 를 주지 않으면 전부 받는다 — 그것이 보통의 길이다.\n *\n * 더해지는 부품은 **뒤에 붙인다.** 후보가 놓은 자리에 끼워 넣으면, 일부만 골랐을\n * 때 순서가 어디서 온 것인지 알 수 없게 된다.\n */\nexport function applyProposal(\n current: FigureSource | undefined,\n proposal: FigureSource,\n picked?: Set<string>\n): FigureSource {\n const takes = (change: ProposalChange) => !picked || picked.has(change.key)\n const changes = diffProposal(current, proposal)\n\n if (!current) {\n return { ...proposal }\n }\n\n const removed = new Set(changes.filter(c => c.kind === 'removed' && takes(c)).map(c => c.key))\n const replaced = new Map(\n changes.filter(c => c.kind === 'changed' && takes(c)).map(c => [c.key, c.part as FigurePart])\n )\n const added = changes.filter(c => c.kind === 'added' && takes(c)).map(c => c.part as FigurePart)\n\n const parts = [\n ...current.parts.filter(part => !removed.has(part.name)).map(part => replaced.get(part.name) ?? part),\n ...added\n ]\n\n // 고른 필드만 후보 값으로 바꾼다. 안 고른 것은 지금 값이 남는다.\n const fields: Record<string, unknown> = {}\n for (const field of FIGURE_FIELDS) {\n const change = changes.find(c => c.key === field)\n if (change && takes(change)) {\n fields[field] = proposal[field]\n }\n }\n\n return { ...current, ...fields, parts } as FigureSource\n}\n\n/** 전부 받는다. */\nexport function takeProposal(current: FigureSource | undefined, proposal: FigureSource): FigureSource {\n return applyProposal(current, proposal)\n}\n"]}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js';
|
|
2
2
|
import '../modeller/figure-side.js';
|
|
3
|
-
import '../modeller/figure-ask.js';
|
|
4
3
|
import '../modeller/figure-canvas.js';
|
|
5
4
|
import '../modeller/figure-preview.js';
|
|
6
5
|
import '../modeller/figure-parts.js';
|
|
@@ -21,7 +20,7 @@ declare const FigureModellerPageBase: typeof PageView;
|
|
|
21
20
|
*
|
|
22
21
|
* ## 편집 데이터는 씬 모델 하나뿐이다
|
|
23
22
|
*
|
|
24
|
-
* 이 페이지가 갖는 것은
|
|
23
|
+
* 이 페이지가 갖는 것은 초안 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.
|
|
25
24
|
* 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,
|
|
26
25
|
* AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.
|
|
27
26
|
*
|
|
@@ -34,7 +33,7 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
34
33
|
/** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */
|
|
35
34
|
private figure?;
|
|
36
35
|
/** 편집 데이터 — 미리보기에 그대로 세워지는 씬 모델이다. */
|
|
37
|
-
private
|
|
36
|
+
private draft?;
|
|
38
37
|
private parts;
|
|
39
38
|
private selected;
|
|
40
39
|
private view;
|
|
@@ -88,7 +87,7 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
88
87
|
/**
|
|
89
88
|
* 페이지 밖 AI 도크에도 **지금 손에 든 정본**을 알려 준다.
|
|
90
89
|
*
|
|
91
|
-
*
|
|
90
|
+
* asidebar 도크는 주소의 id만
|
|
92
91
|
* 알았으므로 DB에 저장된 옛 판을 읽었다. 저장 전 변경을 두고 "왜 발행이 안 되나" 또는
|
|
93
92
|
* "이 부품을 줄여 달라"고 물으면 대화가 엉뚱한 판을 보게 되는 결함이었다.
|
|
94
93
|
*
|
|
@@ -103,15 +102,15 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
103
102
|
/**
|
|
104
103
|
* A candidate from the dock opens the review lane. It does not become the draft.
|
|
105
104
|
*
|
|
106
|
-
* This used to write `
|
|
105
|
+
* This used to write `draft` and `parts` straight onto the working draft and then set
|
|
107
106
|
* `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
|
|
108
107
|
* erased the lane built to review it. There was no moment where the person said yes, and a
|
|
109
108
|
* draft has no version history to go back to, so the previous work was simply gone.
|
|
110
109
|
*
|
|
111
110
|
* `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
|
|
112
111
|
* and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
|
|
113
|
-
*
|
|
114
|
-
* the same door. Applying is what the take button does, from the lane, per change.
|
|
112
|
+
* The in-page entrance honoured this through `openAiProposal` before it was removed, and the dock
|
|
113
|
+
* entrance is now the same door. Applying is what the take button does, from the lane, per change.
|
|
115
114
|
*
|
|
116
115
|
* ## Why the overwrite was there — so it does not come back
|
|
117
116
|
*
|
|
@@ -212,7 +211,7 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
212
211
|
/**
|
|
213
212
|
* 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
|
|
214
213
|
* `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
|
|
215
|
-
* 원본 `
|
|
214
|
+
* 원본 `draft`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
|
|
216
215
|
*/
|
|
217
216
|
private previewSizingFix;
|
|
218
217
|
/**
|
|
@@ -289,12 +288,12 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
289
288
|
* 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
|
|
290
289
|
* 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
|
|
291
290
|
*/
|
|
292
|
-
private
|
|
291
|
+
private updateDraft;
|
|
293
292
|
/**
|
|
294
293
|
* 디테일 등급을 바꾼다.
|
|
295
294
|
*
|
|
296
295
|
* 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라
|
|
297
|
-
*
|
|
296
|
+
* 초안에 담는다.
|
|
298
297
|
*
|
|
299
298
|
* 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.
|
|
300
299
|
* 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지
|