@things-factory/figure-ui 10.1.16 → 10.1.18
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 +331 -124
- 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 +151 -41
- 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 +62 -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 +27 -4
- package/client/pages/figure-modeller-page.ts +65 -58
- package/dist-client/modeller/figure-animations.d.ts +37 -8
- package/dist-client/modeller/figure-animations.js +293 -122
- 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 +56 -6
- package/dist-client/modeller/figure-preview.js +132 -39
- 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 +25 -15
- package/dist-client/modeller/figure-source.js +42 -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 +16 -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 +61 -54
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/ai-proposal-contract.test.ts +433 -12
- package/test/expect.ts +55 -0
- package/test/figure-source.test.ts +572 -0
- package/test/i18n-prefix-guard.test.ts +9 -12
- package/{client/modeller → test}/part-edits.test.ts +50 -45
- package/translations/en.json +46 -35
- package/translations/ja.json +16 -21
- package/translations/ko.json +56 -45
- 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
|
@@ -6,7 +6,7 @@ import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePl
|
|
|
6
6
|
* ## Why this is the only place that converts
|
|
7
7
|
*
|
|
8
8
|
* While authoring, the source of truth is the **scene model** -- part
|
|
9
|
-
* components sitting on a
|
|
9
|
+
* components sitting on a draft. `FigureSource` is produced from it when the
|
|
10
10
|
* asset is saved or scored.
|
|
11
11
|
*
|
|
12
12
|
* The two use different vocabularies. The scene inherited 2D canvas terms
|
|
@@ -27,21 +27,21 @@ import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePl
|
|
|
27
27
|
*
|
|
28
28
|
* ## The mapping
|
|
29
29
|
*
|
|
30
|
-
* The
|
|
30
|
+
* The draft is the base box; its centre is the format's origin.
|
|
31
31
|
*
|
|
32
32
|
* format scene
|
|
33
33
|
* ──────────────── ──────────────────────────────
|
|
34
|
-
* base.x
|
|
35
|
-
* base.y
|
|
36
|
-
* base.z
|
|
34
|
+
* base.x draft width
|
|
35
|
+
* base.y draft depth <- height
|
|
36
|
+
* base.z draft height
|
|
37
37
|
*
|
|
38
38
|
* size.x width
|
|
39
39
|
* size.y depth
|
|
40
40
|
* size.z height
|
|
41
41
|
*
|
|
42
|
-
* position.x left + width/2 -
|
|
43
|
-
* position.y zPos + depth/2 -
|
|
44
|
-
* position.z top + height/2 -
|
|
42
|
+
* position.x left + width/2 - draft width/2
|
|
43
|
+
* position.y zPos + depth/2 - draft depth/2 <- zPos is the underside
|
|
44
|
+
* position.z top + height/2 - draft height/2
|
|
45
45
|
*
|
|
46
46
|
* rotation.x rotationX (scene radians, format degrees)
|
|
47
47
|
* rotation.y -rotation
|
|
@@ -73,7 +73,7 @@ import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePl
|
|
|
73
73
|
* of the four.
|
|
74
74
|
*/
|
|
75
75
|
/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */
|
|
76
|
-
export interface
|
|
76
|
+
export interface FigureDraft {
|
|
77
77
|
/** 형식 버전. */
|
|
78
78
|
version?: number
|
|
79
79
|
/*
|
|
@@ -102,6 +102,14 @@ export interface BoardModel {
|
|
|
102
102
|
* 저장하는 것만으로 **조용히 지워지기** 때문이다.
|
|
103
103
|
*/
|
|
104
104
|
animations?: FigureSource['animations']
|
|
105
|
+
/**
|
|
106
|
+
* 값이 만드는 자세 — **애니메이션과 다른 칸이다.**
|
|
107
|
+
*
|
|
108
|
+
* 저것은 시각이 흘러 움직이는 것(도는 롤러)이고 이것은 인스턴스가 준 값이 그대로 자세가
|
|
109
|
+
* 되는 것(1200mm 내려온 호이스트)이다. 한동안 애니메이션 안에 `drive: 'hold'` 로 숨어
|
|
110
|
+
* 있었고, 그래서 「길이값을 변수로 노출하라」는 요청에 아무도 이것을 못 찾았다(ADR-0051).
|
|
111
|
+
*/
|
|
112
|
+
parameters?: FigureSource['parameters']
|
|
105
113
|
/**
|
|
106
114
|
* 이 도형이 지는 능력. 씬의 capability mixin 이름이다.
|
|
107
115
|
*
|
|
@@ -199,7 +207,7 @@ function withoutEmpty<T extends object>(value: T): T {
|
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */
|
|
202
|
-
function round4(value: number): number {
|
|
210
|
+
export function round4(value: number): number {
|
|
203
211
|
return Math.round(value * 1e4) / 1e4
|
|
204
212
|
}
|
|
205
213
|
|
|
@@ -208,10 +216,30 @@ function round4(value: number): number {
|
|
|
208
216
|
*
|
|
209
217
|
* 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.
|
|
210
218
|
*/
|
|
211
|
-
export function halfOf(
|
|
212
|
-
return { x:
|
|
219
|
+
export function halfOf(draft: FigureDraft): Vec3 {
|
|
220
|
+
return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 }
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
/*
|
|
224
|
+
* ── The placement is not in these numbers ────────────────────────────────────
|
|
225
|
+
*
|
|
226
|
+
* `placement` says which reference surface a figure of this kind attaches to — a conveyor stands
|
|
227
|
+
* on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the
|
|
228
|
+
* scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at
|
|
229
|
+
* the ceiling height less the figure's own height. Nothing about the authored shape changes.
|
|
230
|
+
*
|
|
231
|
+
* So the conversion below does not read it, and the editing canvas does not either. The box is
|
|
232
|
+
* always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical
|
|
233
|
+
* parts and different placements have identical `parts` arrays, which is what the format says.
|
|
234
|
+
*
|
|
235
|
+
* This used to be wired the other way: the canvas fed the figure's placement to the model layer's
|
|
236
|
+
* `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that
|
|
237
|
+
* flips what `zPos` names for everything in that layer. It stood in for the archetype because the
|
|
238
|
+
* modeller holds exactly one figure — and it cost a whole day of defects, because every number the
|
|
239
|
+
* author typed then meant a different face depending on a dropdown. That coordinate mode is now
|
|
240
|
+
* where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.
|
|
241
|
+
*/
|
|
242
|
+
|
|
215
243
|
/**
|
|
216
244
|
* 저작자가 보는 축이 씬의 어느 용어인가.
|
|
217
245
|
*
|
|
@@ -227,34 +255,35 @@ export const AXIS_KEYS = {
|
|
|
227
255
|
export type PlaneAxis = keyof typeof AXIS_KEYS
|
|
228
256
|
|
|
229
257
|
/** 판이 그 축으로 얼마나 넓은가. */
|
|
230
|
-
export function
|
|
231
|
-
return axis === 'x' ?
|
|
258
|
+
export function draftExtent(draft: FigureDraft, axis: PlaneAxis): number {
|
|
259
|
+
return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height
|
|
232
260
|
}
|
|
233
261
|
|
|
234
262
|
/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */
|
|
235
|
-
export function partToFigure(part: PartModel,
|
|
236
|
-
return partTo(part, halfOf(
|
|
263
|
+
export function partToFigure(part: PartModel, draft: FigureDraft): FigurePart {
|
|
264
|
+
return partTo(part, halfOf(draft))
|
|
237
265
|
}
|
|
238
266
|
|
|
239
267
|
/** 부품 하나를 씬 용어로 되돌린다. */
|
|
240
|
-
export function partFromFigure(part: FigurePart,
|
|
241
|
-
return partFrom(part, halfOf(
|
|
268
|
+
export function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel {
|
|
269
|
+
return partFrom(part, halfOf(draft))
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
/** 씬 모델을 저장 형식으로 변환한다. */
|
|
245
|
-
export function toFigureSource(
|
|
246
|
-
const half = halfOf(
|
|
273
|
+
export function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource {
|
|
274
|
+
const half = halfOf(draft)
|
|
247
275
|
|
|
248
276
|
return withoutEmpty({
|
|
249
|
-
version:
|
|
250
|
-
type:
|
|
251
|
-
base: { x:
|
|
252
|
-
placement:
|
|
253
|
-
detailLevel:
|
|
254
|
-
styleKit:
|
|
277
|
+
version: draft.version ?? FIGURE_SOURCE_VERSION,
|
|
278
|
+
type: draft.figureType ?? '',
|
|
279
|
+
base: { x: draft.width, y: draft.depth, z: draft.height },
|
|
280
|
+
placement: draft.placement,
|
|
281
|
+
detailLevel: draft.detailLevel,
|
|
282
|
+
styleKit: draft.styleKit,
|
|
255
283
|
parts: parts.map(part => partTo(part, half)),
|
|
256
|
-
animations:
|
|
257
|
-
|
|
284
|
+
animations: draft.animations,
|
|
285
|
+
parameters: draft.parameters,
|
|
286
|
+
capabilities: draft.capabilities
|
|
258
287
|
}) as FigureSource
|
|
259
288
|
}
|
|
260
289
|
|
|
@@ -308,10 +337,10 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
308
337
|
}
|
|
309
338
|
|
|
310
339
|
/** 형식을 씬 모델로 편다. */
|
|
311
|
-
export function fromFigureSource(source: FigureSource): {
|
|
340
|
+
export function fromFigureSource(source: FigureSource): { draft: FigureDraft; parts: PartModel[] } {
|
|
312
341
|
const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }
|
|
313
342
|
|
|
314
|
-
const
|
|
343
|
+
const draft = withoutEmpty({
|
|
315
344
|
version: source.version,
|
|
316
345
|
width: source.base.x,
|
|
317
346
|
height: source.base.z,
|
|
@@ -321,10 +350,11 @@ export function fromFigureSource(source: FigureSource): { board: BoardModel; par
|
|
|
321
350
|
detailLevel: source.detailLevel,
|
|
322
351
|
styleKit: source.styleKit,
|
|
323
352
|
animations: source.animations,
|
|
353
|
+
parameters: source.parameters,
|
|
324
354
|
capabilities: source.capabilities
|
|
325
|
-
}) as
|
|
355
|
+
}) as FigureDraft
|
|
326
356
|
|
|
327
|
-
return {
|
|
357
|
+
return { draft, parts: source.parts.map(part => partFrom(part, half)) }
|
|
328
358
|
}
|
|
329
359
|
|
|
330
360
|
function partFrom(part: FigurePart, half: Vec3): PartModel {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './figure-canvas.js'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**
|
|
@@ -28,7 +28,7 @@ const WIDTH = 480
|
|
|
28
28
|
const HEIGHT = 360
|
|
29
29
|
|
|
30
30
|
interface Shooter extends HTMLElement {
|
|
31
|
-
|
|
31
|
+
draft?: FigureDraft
|
|
32
32
|
parts?: PartModel[]
|
|
33
33
|
viewOnly?: boolean
|
|
34
34
|
snapshot?: (width?: number, height?: number) => Promise<string | undefined>
|
|
@@ -71,10 +71,10 @@ function within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export async function captureThumbnail(
|
|
74
|
-
|
|
74
|
+
draft: FigureDraft | undefined,
|
|
75
75
|
parts: PartModel[]
|
|
76
76
|
): Promise<string | undefined> {
|
|
77
|
-
if (!
|
|
77
|
+
if (!draft || !parts.length) {
|
|
78
78
|
return undefined
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -89,7 +89,7 @@ export async function captureThumbnail(
|
|
|
89
89
|
|
|
90
90
|
const shooter = document.createElement('figure-canvas') as Shooter
|
|
91
91
|
shooter.viewOnly = true
|
|
92
|
-
shooter.
|
|
92
|
+
shooter.draft = draft
|
|
93
93
|
shooter.parts = parts
|
|
94
94
|
shooter.style.cssText = 'display: block; width: 100%; height: 100%;'
|
|
95
95
|
|
|
@@ -19,6 +19,30 @@
|
|
|
19
19
|
* 한동안 3D 를 세울 때만 읽혔다 — things-scene 10.1.5 에서 고쳤다.
|
|
20
20
|
*/
|
|
21
21
|
export interface ViewSettings {
|
|
22
|
+
/**
|
|
23
|
+
* 어떤 보드에 놓인 것으로 보나 — 씬 전체의 좌표 모드.
|
|
24
|
+
*
|
|
25
|
+
* **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에
|
|
26
|
+
* 매달리나」이고, 이것은 「지금 보고 있는 보드가 어느 쪽을 기준으로 재나」다. 바닥
|
|
27
|
+
* 기준 보드에 천정 기반 OHT 를 놓으면 보드는 안 뒤집히고 OHT 가 천장 높이에 매달린다.
|
|
28
|
+
*
|
|
29
|
+
* 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라
|
|
30
|
+
* 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 보드에서는 성립할 수가
|
|
31
|
+
* 없고(보드에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔
|
|
32
|
+
* 높이가 전부 다른 숫자로 보였다.
|
|
33
|
+
*
|
|
34
|
+
* 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`
|
|
35
|
+
* 천장 기준. **미리보기에만 먹인다** — 편집 캔버스의 `zPos` 는 저작자가 직접 적는
|
|
36
|
+
* 숫자라 그 뜻이 보기 설정에 따라 바뀌면 안 된다.
|
|
37
|
+
*/
|
|
38
|
+
placement: 'floor' | 'space' | 'inverted'
|
|
39
|
+
/**
|
|
40
|
+
* 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.
|
|
41
|
+
*
|
|
42
|
+
* 도형은 제가 놓일 방의 높이를 모른다. 보드는 알지만 미리보기에는 보드가 없으므로,
|
|
43
|
+
* 저작자가 가정할 값을 여기서 정한다.
|
|
44
|
+
*/
|
|
45
|
+
ceilingHeight: number
|
|
22
46
|
/** 좌표 바탕(축 가이드). 원점과 눈금을 보인다. */
|
|
23
47
|
grid: boolean
|
|
24
48
|
/** 축 이름표(X · Y · Z). 눈금만 두고 글자를 끌 수 있다. */
|
|
@@ -57,6 +81,32 @@ export const ENVIRONMENTS = [
|
|
|
57
81
|
] as const
|
|
58
82
|
export type EnvironmentName = (typeof ENVIRONMENTS)[number]
|
|
59
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**
|
|
86
|
+
*
|
|
87
|
+
* 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.
|
|
88
|
+
* 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),
|
|
89
|
+
* `ceilingHeight` 는 씬이 모르는 이름이다.
|
|
90
|
+
*
|
|
91
|
+
* **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안
|
|
92
|
+
* `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을
|
|
93
|
+
* 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로
|
|
94
|
+
* 그 길로 캔버스에 들어갈 뻔했다.
|
|
95
|
+
*/
|
|
96
|
+
export function sceneLook(view: ViewSettings): Record<string, unknown> {
|
|
97
|
+
const { placement, ceilingHeight, ...look } = view
|
|
98
|
+
return look
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**
|
|
103
|
+
*
|
|
104
|
+
* 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지
|
|
105
|
+
* 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준
|
|
106
|
+
* (inverted)」.
|
|
107
|
+
*/
|
|
108
|
+
export const SCENE_REFERENCES = ['floor', 'space', 'inverted'] as const
|
|
109
|
+
|
|
60
110
|
/** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */
|
|
61
111
|
export const WORKBENCH = '#7c7c7c'
|
|
62
112
|
|
|
@@ -67,6 +117,9 @@ export const WORKBENCH = '#7c7c7c'
|
|
|
67
117
|
* 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.
|
|
68
118
|
*/
|
|
69
119
|
export const DEFAULT_VIEW: ViewSettings = {
|
|
120
|
+
placement: 'floor',
|
|
121
|
+
/* 공장 천장의 흔한 높이. 반도체 fab 의 OHT 레일이 대체로 이 언저리다. */
|
|
122
|
+
ceilingHeight: 4000,
|
|
70
123
|
grid: true,
|
|
71
124
|
gridLabels: true,
|
|
72
125
|
floor: false,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'
|
|
2
2
|
import type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
|
-
import { AXIS_KEYS,
|
|
5
|
-
import type {
|
|
4
|
+
import { AXIS_KEYS, draftExtent } from './figure-source.js'
|
|
5
|
+
import type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 부품 편집 규칙. **씬 모델의 용어로** 다룬다.
|
|
9
9
|
*
|
|
10
|
-
* 편집 중에 기준이 되는 데이터는 씬 모델이다 —
|
|
10
|
+
* 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.
|
|
11
11
|
* 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.
|
|
12
12
|
* FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.
|
|
13
13
|
*
|
|
@@ -27,33 +27,33 @@ function freeName(taken: Set<string>, base: string): string {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** 이
|
|
31
|
-
export function partLimit(
|
|
32
|
-
return PART_LIMIT[(
|
|
30
|
+
/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
|
|
31
|
+
export function partLimit(draft: FigureDraft): number {
|
|
32
|
+
return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* 새 부품 하나를 만든다.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
38
|
+
* 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —
|
|
39
39
|
* 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
|
|
40
40
|
*/
|
|
41
|
-
export function newPart(
|
|
41
|
+
export function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel {
|
|
42
42
|
const taken = new Set(parts.map(part => part.name))
|
|
43
43
|
|
|
44
|
-
const width = Math.max(1, Math.round(
|
|
45
|
-
const height = Math.max(1, Math.round(
|
|
46
|
-
const depth = Math.max(1, Math.round(
|
|
44
|
+
const width = Math.max(1, Math.round(draft.width / 3))
|
|
45
|
+
const height = Math.max(1, Math.round(draft.height / 3))
|
|
46
|
+
const depth = Math.max(1, Math.round(draft.depth / 3))
|
|
47
47
|
|
|
48
48
|
const part: PartModel = {
|
|
49
49
|
type: 'figure-part',
|
|
50
50
|
name: freeName(taken, primitive),
|
|
51
51
|
primitive,
|
|
52
|
-
left: Math.round((
|
|
53
|
-
top: Math.round((
|
|
52
|
+
left: Math.round((draft.width - width) / 2),
|
|
53
|
+
top: Math.round((draft.height - height) / 2),
|
|
54
54
|
width,
|
|
55
55
|
height,
|
|
56
|
-
// 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧
|
|
56
|
+
// 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.
|
|
57
57
|
zPos: 0,
|
|
58
58
|
depth,
|
|
59
59
|
// 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.
|
|
@@ -79,8 +79,8 @@ export function newPart(board: BoardModel, parts: PartModel[], primitive: Primit
|
|
|
79
79
|
return part
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function addPart(
|
|
83
|
-
return [...parts, newPart(
|
|
82
|
+
export function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {
|
|
83
|
+
return [...parts, newPart(draft, parts, primitive)]
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function removePart(parts: PartModel[], index: number): PartModel[] {
|
|
@@ -133,25 +133,25 @@ function putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number
|
|
|
133
133
|
return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** 부품을
|
|
137
|
-
export function centerPart(
|
|
136
|
+
/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */
|
|
137
|
+
export function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
|
|
138
138
|
const part = parts[index]
|
|
139
139
|
if (!part) return parts
|
|
140
140
|
|
|
141
|
-
return putAt(parts, index, axis, Math.round((
|
|
141
|
+
return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
/** 부품을
|
|
144
|
+
/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */
|
|
145
145
|
export function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
|
|
146
146
|
return putAt(parts, index, axis, 0)
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/** 부품을
|
|
150
|
-
export function alignPartToEnd(
|
|
149
|
+
/** 부품을 상자의 끝에 붙인다. */
|
|
150
|
+
export function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
|
|
151
151
|
const part = parts[index]
|
|
152
152
|
if (!part) return parts
|
|
153
153
|
|
|
154
|
-
return putAt(parts, index, axis, Math.round(
|
|
154
|
+
return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
@@ -160,22 +160,22 @@ export function alignPartToEnd(board: BoardModel, parts: PartModel[], index: num
|
|
|
160
160
|
* 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수
|
|
161
161
|
* 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.
|
|
162
162
|
*/
|
|
163
|
-
export function mirrorPart(
|
|
163
|
+
export function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {
|
|
164
164
|
const part = parts[index]
|
|
165
|
-
if (!part || !canMirror(
|
|
165
|
+
if (!part || !canMirror(draft, parts, index, axis)) return parts
|
|
166
166
|
|
|
167
167
|
const copy = JSON.parse(JSON.stringify(part)) as PartModel
|
|
168
168
|
copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)
|
|
169
169
|
|
|
170
170
|
const next = [...parts]
|
|
171
171
|
next.splice(index + 1, 0, copy)
|
|
172
|
-
return putAt(next, index + 1, axis, mirroredAt(
|
|
172
|
+
return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
/** 대칭했을 때의 위치.
|
|
176
|
-
function mirroredAt(
|
|
175
|
+
/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */
|
|
176
|
+
function mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {
|
|
177
177
|
const { at, size } = AXIS_KEYS[axis]
|
|
178
|
-
return Math.round(
|
|
178
|
+
return Math.round(draftExtent(draft, axis) - part[at] - part[size])
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -185,24 +185,24 @@ function mirroredAt(board: BoardModel, part: PartModel, axis: PlaneAxis): number
|
|
|
185
185
|
* 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.
|
|
186
186
|
* 만들어 놓고 알리는 대신 미리 막는다.
|
|
187
187
|
*/
|
|
188
|
-
export function canMirror(
|
|
188
|
+
export function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {
|
|
189
189
|
const part = parts[index]
|
|
190
190
|
if (!part) return false
|
|
191
191
|
|
|
192
|
-
return mirroredAt(
|
|
192
|
+
return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* 부품의 위치와 크기를 격자에 맞춘다.
|
|
197
197
|
*
|
|
198
|
-
* 격자 간격은
|
|
198
|
+
* 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
|
|
199
199
|
* 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.
|
|
200
200
|
*/
|
|
201
|
-
export function snapPart(
|
|
201
|
+
export function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {
|
|
202
202
|
const part = parts[index]
|
|
203
203
|
if (!part) return parts
|
|
204
204
|
|
|
205
|
-
const step = gridStep(
|
|
205
|
+
const step = gridStep(draft)
|
|
206
206
|
const round = (value: number) => Math.round(value / step) * step
|
|
207
207
|
|
|
208
208
|
return put(parts, index, {
|
|
@@ -223,8 +223,8 @@ export function snapPart(board: BoardModel, parts: PartModel[], index: number):
|
|
|
223
223
|
* (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가
|
|
224
224
|
* 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.
|
|
225
225
|
*/
|
|
226
|
-
export function gridStep(
|
|
227
|
-
return niceStep(Math.max(
|
|
226
|
+
export function gridStep(draft: FigureDraft): number {
|
|
227
|
+
return niceStep(Math.max(draft.width, draft.height, draft.depth))
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/**
|
|
@@ -21,20 +21,43 @@ import type { FigurePart, FigureSource } from '@hatiolab/figure-model'
|
|
|
21
21
|
* 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준
|
|
22
22
|
* 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.
|
|
23
23
|
*/
|
|
24
|
-
export type ChangeKind =
|
|
24
|
+
export type ChangeKind =
|
|
25
|
+
| 'added'
|
|
26
|
+
| 'changed'
|
|
27
|
+
| 'removed'
|
|
28
|
+
| 'base'
|
|
29
|
+
| 'placement'
|
|
30
|
+
| 'detail'
|
|
31
|
+
| 'styleKit'
|
|
32
|
+
| 'animations'
|
|
33
|
+
| 'parameters'
|
|
34
|
+
| 'capabilities'
|
|
25
35
|
|
|
26
36
|
/**
|
|
27
37
|
* 부품 밖에서 바뀔 수 있는 것 전부.
|
|
28
38
|
*
|
|
29
39
|
* 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도
|
|
30
40
|
* 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.
|
|
41
|
+
*
|
|
42
|
+
* 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.
|
|
43
|
+
* 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는
|
|
44
|
+
* 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만
|
|
45
|
+
* 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)
|
|
46
|
+
* 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.
|
|
47
|
+
*
|
|
48
|
+
* 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 여섯이 전부다. `type` 은 저장되는
|
|
49
|
+
* 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이
|
|
50
|
+
* 아니라 저장할 때 서버가 찍는다.
|
|
31
51
|
*/
|
|
32
|
-
const FIGURE_FIELDS = ['base', '
|
|
52
|
+
const FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities'] as const
|
|
33
53
|
const FIELD_KIND: Record<(typeof FIGURE_FIELDS)[number], ChangeKind> = {
|
|
34
54
|
base: 'base',
|
|
55
|
+
placement: 'placement',
|
|
35
56
|
detailLevel: 'detail',
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
styleKit: 'styleKit',
|
|
58
|
+
animations: 'animations',
|
|
59
|
+
parameters: 'parameters',
|
|
60
|
+
capabilities: 'capabilities'
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
export interface ProposalChange {
|