@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.
Files changed (77) hide show
  1. package/client/modeller/figure-animations.ts +12 -12
  2. package/client/modeller/figure-canvas.ts +32 -48
  3. package/client/modeller/figure-capabilities.ts +10 -10
  4. package/client/modeller/figure-inspector.ts +39 -28
  5. package/client/modeller/figure-parts.ts +1 -1
  6. package/client/modeller/figure-preview.ts +122 -25
  7. package/client/modeller/figure-report.ts +24 -6
  8. package/client/modeller/figure-settings.ts +45 -2
  9. package/client/modeller/figure-side.ts +6 -6
  10. package/client/modeller/figure-source.ts +52 -32
  11. package/client/modeller/figure-thumbnail.ts +5 -5
  12. package/client/modeller/figure-view.ts +53 -0
  13. package/client/modeller/part-edits.ts +36 -36
  14. package/client/modeller/proposal.ts +25 -4
  15. package/client/pages/figure-modeller-page.ts +64 -58
  16. package/dist-client/modeller/figure-animations.d.ts +4 -4
  17. package/dist-client/modeller/figure-animations.js +10 -10
  18. package/dist-client/modeller/figure-animations.js.map +1 -1
  19. package/dist-client/modeller/figure-canvas.d.ts +7 -7
  20. package/dist-client/modeller/figure-canvas.js +27 -40
  21. package/dist-client/modeller/figure-canvas.js.map +1 -1
  22. package/dist-client/modeller/figure-capabilities.d.ts +2 -2
  23. package/dist-client/modeller/figure-capabilities.js +9 -9
  24. package/dist-client/modeller/figure-capabilities.js.map +1 -1
  25. package/dist-client/modeller/figure-inspector.d.ts +16 -5
  26. package/dist-client/modeller/figure-inspector.js +38 -27
  27. package/dist-client/modeller/figure-inspector.js.map +1 -1
  28. package/dist-client/modeller/figure-parts.d.ts +1 -1
  29. package/dist-client/modeller/figure-parts.js +1 -1
  30. package/dist-client/modeller/figure-parts.js.map +1 -1
  31. package/dist-client/modeller/figure-preview.d.ts +39 -0
  32. package/dist-client/modeller/figure-preview.js +112 -23
  33. package/dist-client/modeller/figure-preview.js.map +1 -1
  34. package/dist-client/modeller/figure-report.d.ts +9 -0
  35. package/dist-client/modeller/figure-report.js +23 -5
  36. package/dist-client/modeller/figure-report.js.map +1 -1
  37. package/dist-client/modeller/figure-settings.js +42 -2
  38. package/dist-client/modeller/figure-settings.js.map +1 -1
  39. package/dist-client/modeller/figure-side.d.ts +2 -2
  40. package/dist-client/modeller/figure-side.js +5 -5
  41. package/dist-client/modeller/figure-side.js.map +1 -1
  42. package/dist-client/modeller/figure-source.d.ts +17 -15
  43. package/dist-client/modeller/figure-source.js +40 -21
  44. package/dist-client/modeller/figure-source.js.map +1 -1
  45. package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
  46. package/dist-client/modeller/figure-thumbnail.js +3 -3
  47. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  48. package/dist-client/modeller/figure-view.d.ts +45 -0
  49. package/dist-client/modeller/figure-view.js +27 -0
  50. package/dist-client/modeller/figure-view.js.map +1 -1
  51. package/dist-client/modeller/part-edits.d.ts +16 -16
  52. package/dist-client/modeller/part-edits.js +35 -35
  53. package/dist-client/modeller/part-edits.js.map +1 -1
  54. package/dist-client/modeller/proposal.d.ts +1 -1
  55. package/dist-client/modeller/proposal.js +15 -3
  56. package/dist-client/modeller/proposal.js.map +1 -1
  57. package/dist-client/pages/figure-modeller-page.d.ts +9 -10
  58. package/dist-client/pages/figure-modeller-page.js +60 -54
  59. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  60. package/dist-client/tsconfig.tsbuildinfo +1 -1
  61. package/dist-server/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +4 -4
  63. package/test/ai-proposal-contract.test.ts +393 -12
  64. package/test/expect.ts +55 -0
  65. package/test/figure-source.test.ts +562 -0
  66. package/test/i18n-prefix-guard.test.ts +1 -1
  67. package/{client/modeller → test}/part-edits.test.ts +50 -45
  68. package/translations/en.json +31 -28
  69. package/translations/ja.json +16 -21
  70. package/translations/ko.json +41 -38
  71. package/translations/ms.json +16 -21
  72. package/translations/zh.json +16 -21
  73. package/client/modeller/figure-ask.ts +0 -265
  74. package/client/modeller/figure-source.test.ts +0 -352
  75. package/dist-client/modeller/figure-ask.d.ts +0 -40
  76. package/dist-client/modeller/figure-ask.js +0 -282
  77. package/dist-client/modeller/figure-ask.js.map +0 -1
@@ -5,7 +5,7 @@ import { type DetailLevel, type FigurePart, type FigurePlacement, type FigureSou
5
5
  * ## Why this is the only place that converts
6
6
  *
7
7
  * While authoring, the source of truth is the **scene model** -- part
8
- * components sitting on a board. `FigureSource` is produced from it when the
8
+ * components sitting on a draft. `FigureSource` is produced from it when the
9
9
  * asset is saved or scored.
10
10
  *
11
11
  * The two use different vocabularies. The scene inherited 2D canvas terms
@@ -26,21 +26,21 @@ import { type DetailLevel, type FigurePart, type FigurePlacement, type FigureSou
26
26
  *
27
27
  * ## The mapping
28
28
  *
29
- * The board is the base box; its centre is the format's origin.
29
+ * The draft is the base box; its centre is the format's origin.
30
30
  *
31
31
  * format scene
32
32
  * ──────────────── ──────────────────────────────
33
- * base.x board width
34
- * base.y board depth <- height
35
- * base.z board height
33
+ * base.x draft width
34
+ * base.y draft depth <- height
35
+ * base.z draft height
36
36
  *
37
37
  * size.x width
38
38
  * size.y depth
39
39
  * size.z height
40
40
  *
41
- * position.x left + width/2 - board width/2
42
- * position.y zPos + depth/2 - board depth/2 <- zPos is the underside
43
- * position.z top + height/2 - board height/2
41
+ * position.x left + width/2 - draft width/2
42
+ * position.y zPos + depth/2 - draft depth/2 <- zPos is the underside
43
+ * position.z top + height/2 - draft height/2
44
44
  *
45
45
  * rotation.x rotationX (scene radians, format degrees)
46
46
  * rotation.y -rotation
@@ -72,7 +72,7 @@ import { type DetailLevel, type FigurePart, type FigurePlacement, type FigureSou
72
72
  * of the four.
73
73
  */
74
74
  /** 판 컴포넌트의 상태. 씬의 용어 그대로다. */
75
- export interface BoardModel {
75
+ export interface FigureDraft {
76
76
  /** 형식 버전. */
77
77
  version?: number;
78
78
  /** 가로 너비 (mm) → `base.x`. */
@@ -168,12 +168,14 @@ export interface PartModel {
168
168
  */
169
169
  capability?: FigurePart['capability'];
170
170
  }
171
+ /** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */
172
+ export declare function round4(value: number): number;
171
173
  /**
172
174
  * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.
173
175
  *
174
176
  * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.
175
177
  */
176
- export declare function halfOf(board: BoardModel): Vec3;
178
+ export declare function halfOf(draft: FigureDraft): Vec3;
177
179
  /**
178
180
  * 저작자가 보는 축이 씬의 어느 용어인가.
179
181
  *
@@ -196,15 +198,15 @@ export declare const AXIS_KEYS: {
196
198
  };
197
199
  export type PlaneAxis = keyof typeof AXIS_KEYS;
198
200
  /** 판이 그 축으로 얼마나 넓은가. */
199
- export declare function boardExtent(board: BoardModel, axis: PlaneAxis): number;
201
+ export declare function draftExtent(draft: FigureDraft, axis: PlaneAxis): number;
200
202
  /** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */
201
- export declare function partToFigure(part: PartModel, board: BoardModel): FigurePart;
203
+ export declare function partToFigure(part: PartModel, draft: FigureDraft): FigurePart;
202
204
  /** 부품 하나를 씬 용어로 되돌린다. */
203
- export declare function partFromFigure(part: FigurePart, board: BoardModel): PartModel;
205
+ export declare function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel;
204
206
  /** 씬 모델을 저장 형식으로 변환한다. */
205
- export declare function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSource;
207
+ export declare function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource;
206
208
  /** 형식을 씬 모델로 편다. */
207
209
  export declare function fromFigureSource(source: FigureSource): {
208
- board: BoardModel;
210
+ draft: FigureDraft;
209
211
  parts: PartModel[];
210
212
  };
@@ -11,7 +11,7 @@ function withoutEmpty(value) {
11
11
  return out;
12
12
  }
13
13
  /** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */
14
- function round4(value) {
14
+ export function round4(value) {
15
15
  return Math.round(value * 1e4) / 1e4;
16
16
  }
17
17
  /**
@@ -19,9 +19,28 @@ function round4(value) {
19
19
  *
20
20
  * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.
21
21
  */
22
- export function halfOf(board) {
23
- return { x: board.width / 2, y: board.depth / 2, z: board.height / 2 };
22
+ export function halfOf(draft) {
23
+ return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 };
24
24
  }
25
+ /*
26
+ * ── The placement is not in these numbers ────────────────────────────────────
27
+ *
28
+ * `placement` says which reference surface a figure of this kind attaches to — a conveyor stands
29
+ * on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the
30
+ * scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at
31
+ * the ceiling height less the figure's own height. Nothing about the authored shape changes.
32
+ *
33
+ * So the conversion below does not read it, and the editing canvas does not either. The box is
34
+ * always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical
35
+ * parts and different placements have identical `parts` arrays, which is what the format says.
36
+ *
37
+ * This used to be wired the other way: the canvas fed the figure's placement to the model layer's
38
+ * `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that
39
+ * flips what `zPos` names for everything in that layer. It stood in for the archetype because the
40
+ * modeller holds exactly one figure — and it cost a whole day of defects, because every number the
41
+ * author typed then meant a different face depending on a dropdown. That coordinate mode is now
42
+ * where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.
43
+ */
25
44
  /**
26
45
  * 저작자가 보는 축이 씬의 어느 용어인가.
27
46
  *
@@ -34,30 +53,30 @@ export const AXIS_KEYS = {
34
53
  z: { at: 'top', size: 'height' }
35
54
  };
36
55
  /** 판이 그 축으로 얼마나 넓은가. */
37
- export function boardExtent(board, axis) {
38
- return axis === 'x' ? board.width : axis === 'y' ? board.depth : board.height;
56
+ export function draftExtent(draft, axis) {
57
+ return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height;
39
58
  }
40
59
  /** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */
41
- export function partToFigure(part, board) {
42
- return partTo(part, halfOf(board));
60
+ export function partToFigure(part, draft) {
61
+ return partTo(part, halfOf(draft));
43
62
  }
44
63
  /** 부품 하나를 씬 용어로 되돌린다. */
45
- export function partFromFigure(part, board) {
46
- return partFrom(part, halfOf(board));
64
+ export function partFromFigure(part, draft) {
65
+ return partFrom(part, halfOf(draft));
47
66
  }
48
67
  /** 씬 모델을 저장 형식으로 변환한다. */
49
- export function toFigureSource(board, parts) {
50
- const half = halfOf(board);
68
+ export function toFigureSource(draft, parts) {
69
+ const half = halfOf(draft);
51
70
  return withoutEmpty({
52
- version: board.version ?? FIGURE_SOURCE_VERSION,
53
- type: board.figureType ?? '',
54
- base: { x: board.width, y: board.depth, z: board.height },
55
- placement: board.placement,
56
- detailLevel: board.detailLevel,
57
- styleKit: board.styleKit,
71
+ version: draft.version ?? FIGURE_SOURCE_VERSION,
72
+ type: draft.figureType ?? '',
73
+ base: { x: draft.width, y: draft.depth, z: draft.height },
74
+ placement: draft.placement,
75
+ detailLevel: draft.detailLevel,
76
+ styleKit: draft.styleKit,
58
77
  parts: parts.map(part => partTo(part, half)),
59
- animations: board.animations,
60
- capabilities: board.capabilities
78
+ animations: draft.animations,
79
+ capabilities: draft.capabilities
61
80
  });
62
81
  }
63
82
  function partTo(part, half) {
@@ -109,7 +128,7 @@ function partTo(part, half) {
109
128
  /** 형식을 씬 모델로 편다. */
110
129
  export function fromFigureSource(source) {
111
130
  const half = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 };
112
- const board = withoutEmpty({
131
+ const draft = withoutEmpty({
113
132
  version: source.version,
114
133
  width: source.base.x,
115
134
  height: source.base.z,
@@ -121,7 +140,7 @@ export function fromFigureSource(source) {
121
140
  animations: source.animations,
122
141
  capabilities: source.capabilities
123
142
  });
124
- return { board, parts: source.parts.map(part => partFrom(part, half)) };
143
+ return { draft, parts: source.parts.map(part => partFrom(part, half)) };
125
144
  }
126
145
  function partFrom(part, half) {
127
146
  const { position, size, rotation } = part.transform;
@@ -1 +1 @@
1
- {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a board. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The board is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x board width\n * base.y board depth <- height\n * base.z board height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - board width/2\n * position.y zPos + depth/2 - board depth/2 <- zPos is the underside\n * position.z top + height/2 - board height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface BoardModel {\n /** 형식 버전. */\n version?: number\n /*\n The three sizes keep the scene component's names, and the scene assigns its axes differently\n from the format: here `depth` is the vertical one. Naming the axis alone (\"the y axis\") does\n not say which of the two conventions is meant, and this interface sits on the boundary between\n them — so each field names the `FigureSource` field it becomes. `toFigureSource` and\n `fromFigureSource` below are the only places that may disagree with these three lines.\n */\n /** 가로 너비 (mm) → `base.x`. */\n width: number\n /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */\n height: number\n /** 높이 (mm) → `base.y`. 서 있는 방향이다. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n /*\n `FigurePart['material']` is required in the format, so these index into it directly. The\n `NonNullable` on `shape` above is not the same case — `shape` is optional there.\n */\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nfunction round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(board: BoardModel): Vec3 {\n return { x: board.width / 2, y: board.depth / 2, z: board.height / 2 }\n}\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function boardExtent(board: BoardModel, axis: PlaneAxis): number {\n return axis === 'x' ? board.width : axis === 'y' ? board.depth : board.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, board: BoardModel): FigurePart {\n return partTo(part, halfOf(board))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, board: BoardModel): PartModel {\n return partFrom(part, halfOf(board))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSource {\n const half = halfOf(board)\n\n return withoutEmpty({\n version: board.version ?? FIGURE_SOURCE_VERSION,\n type: board.figureType ?? '',\n base: { x: board.width, y: board.depth, z: board.height },\n placement: board.placement,\n detailLevel: board.detailLevel,\n styleKit: board.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: board.animations,\n capabilities: board.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { board: BoardModel; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const board = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n capabilities: source.capabilities\n }) as BoardModel\n\n return { board, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
1
+ {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAyF,MAAM,wBAAwB,CAAA;AA4LrJ,MAAM,UAAU,GAAG,GAAG,GAAG,IAAI,CAAC,EAAE,CAAA;AAChC,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,GAAG,GAAG,CAAA;AAEhC,+DAA+D;AAC/D,SAAS,YAAY,CAAmB,KAAQ;IAC9C,MAAM,GAAG,GAAG,EAA6B,CAAA;IACzC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChD,IAAI,IAAI,KAAK,SAAS;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA;IACzC,CAAC;IACD,OAAO,GAAQ,CAAA;AACjB,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,KAAkB;IACvC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAA;AACxE,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;IAChC,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;IAChC,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE;CACxB,CAAA;AAIV,wBAAwB;AACxB,MAAM,UAAU,WAAW,CAAC,KAAkB,EAAE,IAAe;IAC7D,OAAO,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAA;AAC/E,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,YAAY,CAAC,IAAe,EAAE,KAAkB;IAC9D,OAAO,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;AACpC,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,cAAc,CAAC,IAAgB,EAAE,KAAkB;IACjE,OAAO,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;AACtC,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,cAAc,CAAC,KAAkB,EAAE,KAAkB;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE1B,OAAO,YAAY,CAAC;QAClB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,qBAAqB;QAC/C,IAAI,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE;QAC5B,IAAI,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE;QACzD,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CAAiB,CAAA;AACpB,CAAC;AAED,SAAS,MAAM,CAAC,IAAe,EAAE,IAAU;IACzC,MAAM,QAAQ,GAAG,YAAY,CAAC;QAC5B,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;QACnE,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;QAClE,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;KACpE,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IACvF;;;;;;MAME;IACF,MAAM,QAAQ,GAAG,YAAY,CAAC;QAC5B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAA;IAEF,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,YAAY,CAAC;YACtB,QAAQ,EAAE;gBACR,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;gBAC9C,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;gBAC9C,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;aAC/C;YACD,IAAI,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE;YACtD,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;SAClE,CAAC;QACF,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACxD,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;QACjE,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAe,CAAA;AAClB,CAAC;AAED,oBAAoB;AACpB,MAAM,UAAU,gBAAgB,CAAC,MAAoB;IACnD,MAAM,IAAI,GAAS,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAA;IAEvF,MAAM,KAAK,GAAG,YAAY,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC,IAAI;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;KAClC,CAAgB,CAAA;IAEjB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAA;AACzE,CAAC;AAED,SAAS,QAAQ,CAAC,IAAgB,EAAE,IAAU;IAC5C,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAA;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAA;IAEpC,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;QAEzB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC9C,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7C,KAAK,EAAE,IAAI,CAAC,CAAC;QACb,MAAM,EAAE,IAAI,CAAC,CAAC;QACd,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC9C,KAAK,EAAE,IAAI,CAAC,CAAC;QAEb;;;;;UAKE;QACF,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS;QAC5D,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS;QAC5D,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS;QAE5D,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK;QACxB,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI;QACtB,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM;QAE1B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,OAAO,EAAE,QAAQ,CAAC,OAAO;QAEzB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAc,CAAA;AACjB,CAAC","sourcesContent":["import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a draft. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The draft is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x draft width\n * base.y draft depth <- height\n * base.z draft height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - draft width/2\n * position.y zPos + depth/2 - draft depth/2 <- zPos is the underside\n * position.z top + height/2 - draft height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface FigureDraft {\n /** 형식 버전. */\n version?: number\n /*\n The three sizes keep the scene component's names, and the scene assigns its axes differently\n from the format: here `depth` is the vertical one. Naming the axis alone (\"the y axis\") does\n not say which of the two conventions is meant, and this interface sits on the boundary between\n them — so each field names the `FigureSource` field it becomes. `toFigureSource` and\n `fromFigureSource` below are the only places that may disagree with these three lines.\n */\n /** 가로 너비 (mm) → `base.x`. */\n width: number\n /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */\n height: number\n /** 높이 (mm) → `base.y`. 서 있는 방향이다. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n /*\n `FigurePart['material']` is required in the format, so these index into it directly. The\n `NonNullable` on `shape` above is not the same case — `shape` is optional there.\n */\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nexport function round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(draft: FigureDraft): Vec3 {\n return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 }\n}\n\n/*\n * ── The placement is not in these numbers ────────────────────────────────────\n *\n * `placement` says which reference surface a figure of this kind attaches to — a conveyor stands\n * on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the\n * scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at\n * the ceiling height less the figure's own height. Nothing about the authored shape changes.\n *\n * So the conversion below does not read it, and the editing canvas does not either. The box is\n * always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical\n * parts and different placements have identical `parts` arrays, which is what the format says.\n *\n * This used to be wired the other way: the canvas fed the figure's placement to the model layer's\n * `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that\n * flips what `zPos` names for everything in that layer. It stood in for the archetype because the\n * modeller holds exactly one figure — and it cost a whole day of defects, because every number the\n * author typed then meant a different face depending on a dropdown. That coordinate mode is now\n * where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.\n */\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function draftExtent(draft: FigureDraft, axis: PlaneAxis): number {\n return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, draft: FigureDraft): FigurePart {\n return partTo(part, halfOf(draft))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel {\n return partFrom(part, halfOf(draft))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource {\n const half = halfOf(draft)\n\n return withoutEmpty({\n version: draft.version ?? FIGURE_SOURCE_VERSION,\n type: draft.figureType ?? '',\n base: { x: draft.width, y: draft.depth, z: draft.height },\n placement: draft.placement,\n detailLevel: draft.detailLevel,\n styleKit: draft.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: draft.animations,\n capabilities: draft.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { draft: FigureDraft; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const draft = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n capabilities: source.capabilities\n }) as FigureDraft\n\n return { draft, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
@@ -1,3 +1,3 @@
1
1
  import './figure-canvas.js';
2
- import type { BoardModel, PartModel } from './figure-source.js';
3
- export declare function captureThumbnail(board: BoardModel | undefined, parts: PartModel[]): Promise<string | undefined>;
2
+ import type { FigureDraft, PartModel } from './figure-source.js';
3
+ export declare function captureThumbnail(draft: FigureDraft | undefined, parts: PartModel[]): Promise<string | undefined>;
@@ -51,8 +51,8 @@ const PATIENCE = 5000;
51
51
  function within(work, ms) {
52
52
  return Promise.race([work, delay(ms).then(() => undefined)]);
53
53
  }
54
- export async function captureThumbnail(board, parts) {
55
- if (!board || !parts.length) {
54
+ export async function captureThumbnail(draft, parts) {
55
+ if (!draft || !parts.length) {
56
56
  return undefined;
57
57
  }
58
58
  /*
@@ -65,7 +65,7 @@ export async function captureThumbnail(board, parts) {
65
65
  document.body.appendChild(host);
66
66
  const shooter = document.createElement('figure-canvas');
67
67
  shooter.viewOnly = true;
68
- shooter.board = board;
68
+ shooter.draft = draft;
69
69
  shooter.parts = parts;
70
70
  shooter.style.cssText = 'display: block; width: 100%; height: 100%;';
71
71
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"figure-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/figure-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AASlB;;;;;;;;;GASG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CACzC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,wDAAwD;AACxD,SAAS,MAAM,CAAI,IAAgB,EAAE,EAAU;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA6B,EAC7B,KAAkB;IAElB,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAY,CAAA;IAClE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAEzB;;;;UAIE;QACF,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7E,CAAC,CAAC,EAAE,EACJ,QAAQ,CACT,CAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QACpE,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './figure-canvas.js'\n\nimport type { BoardModel, PartModel } from './figure-source.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있고, 칸의 비율도 창 크기와 접힌 패널에 따라\n * 다르다. 그것을 찍으면 카드마다 프레임이 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이\n * 된다. 목록은 견주는 자리이므로 그림은 **정해진 크기와 정해진 카메라**로 찍는다.\n *\n * ## 왜 씬을 여기서 세우지 않고 부품을 세우나\n *\n * 씬 값(환경 · 하늘 · 조명 · 픽셀 비율 · 좌표 바탕)은 `figure-canvas` 가 든다. 여기서 다시 적으면\n * 두 벌이 되고, 그러면 **카드에 보이는 그림과 저작면에서 보던 것이 아무 오류 없이 갈린다.** 그래서\n * 같은 부품을 `viewOnly` 로 하나 더 세워 그 부품이 자기 씬을 세우게 하고, 찍은 뒤 버린다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 보드는 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주\n * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\ninterface Shooter extends HTMLElement {\n board?: BoardModel\n parts?: PartModel[]\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n}\n\n/**\n * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.\n *\n * **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면\n * 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,\n * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.\n *\n * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래\n * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**\n */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n\n return Promise.race([frames, delay(600)])\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * 그림을 기다리는 시간의 한도.\n *\n * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가\n * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,\n * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).\n */\nconst PATIENCE = 5000\n\n/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */\nfunction within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {\n return Promise.race([work, delay(ms).then(() => undefined)])\n}\n\nexport async function captureThumbnail(\n board: BoardModel | undefined,\n parts: PartModel[]\n): Promise<string | undefined> {\n if (!board || !parts.length) {\n return undefined\n }\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-canvas') as Shooter\n shooter.viewOnly = true\n shooter.board = board\n shooter.parts = parts\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n\n /*\n **한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을\n 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속\n 안 생기면 그 줄이 단서가 된다.\n */\n const shot = await within(\n (async () => {\n await settle()\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n })(),\n PATIENCE\n )\n\n if (!shot) {\n console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\n }\n\n return shot\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
1
+ {"version":3,"file":"figure-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/figure-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AASlB;;;;;;;;;GASG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CACzC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,wDAAwD;AACxD,SAAS,MAAM,CAAI,IAAgB,EAAE,EAAU;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA8B,EAC9B,KAAkB;IAElB,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAY,CAAA;IAClE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAEzB;;;;UAIE;QACF,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7E,CAAC,CAAC,EAAE,EACJ,QAAQ,CACT,CAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QACpE,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './figure-canvas.js'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있고, 칸의 비율도 창 크기와 접힌 패널에 따라\n * 다르다. 그것을 찍으면 카드마다 프레임이 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이\n * 된다. 목록은 견주는 자리이므로 그림은 **정해진 크기와 정해진 카메라**로 찍는다.\n *\n * ## 왜 씬을 여기서 세우지 않고 부품을 세우나\n *\n * 씬 값(환경 · 하늘 · 조명 · 픽셀 비율 · 좌표 바탕)은 `figure-canvas` 가 든다. 여기서 다시 적으면\n * 두 벌이 되고, 그러면 **카드에 보이는 그림과 저작면에서 보던 것이 아무 오류 없이 갈린다.** 그래서\n * 같은 부품을 `viewOnly` 로 하나 더 세워 그 부품이 자기 씬을 세우게 하고, 찍은 뒤 버린다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 보드는 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주\n * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\ninterface Shooter extends HTMLElement {\n draft?: FigureDraft\n parts?: PartModel[]\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n}\n\n/**\n * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.\n *\n * **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면\n * 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,\n * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.\n *\n * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래\n * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**\n */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n\n return Promise.race([frames, delay(600)])\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * 그림을 기다리는 시간의 한도.\n *\n * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가\n * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,\n * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).\n */\nconst PATIENCE = 5000\n\n/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */\nfunction within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {\n return Promise.race([work, delay(ms).then(() => undefined)])\n}\n\nexport async function captureThumbnail(\n draft: FigureDraft | undefined,\n parts: PartModel[]\n): Promise<string | undefined> {\n if (!draft || !parts.length) {\n return undefined\n }\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-canvas') as Shooter\n shooter.viewOnly = true\n shooter.draft = draft\n shooter.parts = parts\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n\n /*\n **한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을\n 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속\n 안 생기면 그 줄이 단서가 된다.\n */\n const shot = await within(\n (async () => {\n await settle()\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n })(),\n PATIENCE\n )\n\n if (!shot) {\n console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\n }\n\n return shot\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
@@ -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). 눈금만 두고 글자를 끌 수 있다. */
@@ -46,6 +70,27 @@ export interface ViewSettings {
46
70
  */
47
71
  export declare const ENVIRONMENTS: readonly ["studio", "warehouse", "factory", "office", "home", "sunny", "cloudy", "rainy"];
48
72
  export type EnvironmentName = (typeof ENVIRONMENTS)[number];
73
+ /**
74
+ * 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**
75
+ *
76
+ * 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.
77
+ * 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),
78
+ * `ceilingHeight` 는 씬이 모르는 이름이다.
79
+ *
80
+ * **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안
81
+ * `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을
82
+ * 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로
83
+ * 그 길로 캔버스에 들어갈 뻔했다.
84
+ */
85
+ export declare function sceneLook(view: ViewSettings): Record<string, unknown>;
86
+ /**
87
+ * 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**
88
+ *
89
+ * 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지
90
+ * 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준
91
+ * (inverted)」.
92
+ */
93
+ export declare const SCENE_REFERENCES: readonly ["floor", "space", "inverted"];
49
94
  /** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */
50
95
  export declare const WORKBENCH = "#7c7c7c";
51
96
  /**
@@ -14,6 +14,30 @@ export const ENVIRONMENTS = [
14
14
  'cloudy',
15
15
  'rainy'
16
16
  ];
17
+ /**
18
+ * 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**
19
+ *
20
+ * 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.
21
+ * 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),
22
+ * `ceilingHeight` 는 씬이 모르는 이름이다.
23
+ *
24
+ * **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안
25
+ * `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을
26
+ * 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로
27
+ * 그 길로 캔버스에 들어갈 뻔했다.
28
+ */
29
+ export function sceneLook(view) {
30
+ const { placement, ceilingHeight, ...look } = view;
31
+ return look;
32
+ }
33
+ /**
34
+ * 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**
35
+ *
36
+ * 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지
37
+ * 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준
38
+ * (inverted)」.
39
+ */
40
+ export const SCENE_REFERENCES = ['floor', 'space', 'inverted'];
17
41
  /** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */
18
42
  export const WORKBENCH = '#7c7c7c';
19
43
  /**
@@ -23,6 +47,9 @@ export const WORKBENCH = '#7c7c7c';
23
47
  * 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.
24
48
  */
25
49
  export const DEFAULT_VIEW = {
50
+ placement: 'floor',
51
+ /* 공장 천장의 흔한 높이. 반도체 fab 의 OHT 레일이 대체로 이 언저리다. */
52
+ ceilingHeight: 4000,
26
53
  grid: true,
27
54
  gridLabels: true,
28
55
  floor: false,
@@ -1 +1 @@
1
- {"version":3,"file":"figure-view.js","sourceRoot":"","sources":["../../client/modeller/figure-view.ts"],"names":[],"mappings":"AAyCA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,WAAW;IACX,SAAS;IACT,QAAQ;IACR,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;CACC,CAAA;AAGV,qCAAqC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,KAAK;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,SAAS;IACnB,iBAAiB,EAAE,GAAG;IACtB,oBAAoB,EAAE,IAAI;IAC1B,gBAAgB,EAAE,IAAI;IACtB,aAAa,EAAE,GAAG;CACnB,CAAA","sourcesContent":["/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 저장되지 않는다\n *\n * 이것은 자산이 아니라 **작업대**다. 조명을 밝게 하고 바닥을 켠 것은 그 사람이 지금\n * 보기 편한 상태일 뿐, 그 자산의 성질이 아니다. 그래서 `FigureSource` 에 안 들어간다 —\n * `toFigureSource` 는 판에서 `width` · `height` · `depth` 와 몇 가지만 집어 가고 나머지는\n * 버린다.\n *\n * 다른 사람이 같은 자산을 열면 자기 설정으로 본다. 그것이 맞다.\n *\n * ## 씬이 아는 이름을 그대로 쓴다\n *\n * 여기 이름은 전부 `model-layer` 의 상태 이름이다. 옮겨 적는 표를 두지 않는다 — 표를\n * 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n *\n * 값이 바뀌면 씬이 스스로 반영한다(`three-capability` 의 `onchange`). 다만 `grid` 는\n * 한동안 3D 를 세울 때만 읽혔다 — things-scene 10.1.5 에서 고쳤다.\n */\nexport interface ViewSettings {\n /** 좌표 바탕(축 가이드). 원점과 눈금을 보인다. */\n grid: boolean\n /** 축 이름표(X · Y · Z). 눈금만 두고 글자를 끌 수 있다. */\n gridLabels: boolean\n /** 판의 바닥면. 좌표 바탕과 겹치므로 기본은 끈다. */\n floor: boolean\n /** Environment — 반사(IBL). 금속 재질이 무엇을 비출지 정한다. */\n environment: EnvironmentName\n /** Sky Color — 배경색. */\n skyColor: string\n /** Key Light Intensity. */\n dirLightIntensity: number\n /** Key Light — Follow Camera. 끄면 그림자 방향이 고정된다. */\n dirLightFollowCamera: boolean\n /** Key Light — Cast Shadow. */\n dirShadowEnabled: boolean\n /** Hemisphere Light Intensity — 그늘진 면을 들어올린다. */\n hemiIntensity: number\n}\n\n/**\n * 환경 프리셋 — **`scene-manager` 의 `SkyPreset` 과 같은 여덟이다.**\n *\n * 한동안 다섯만 적었다. 씬은 여덟을 다 받는데 이쪽이 셋을 몰라서 고를 수 없었다.\n * operato-board 의 3D 씬 속성 패널도 같은 여덟을 보인다.\n */\nexport const ENVIRONMENTS = [\n 'studio',\n 'warehouse',\n 'factory',\n 'office',\n 'home',\n 'sunny',\n 'cloudy',\n 'rainy'\n] as const\nexport type EnvironmentName = (typeof ENVIRONMENTS)[number]\n\n/** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */\nexport const WORKBENCH = '#7c7c7c'\n\n/**\n * 처음 값.\n *\n * **지금 저작 화면이 실제로 쓰던 값 그대로다.** 설정을 만들면서 화면이 달라지면\n * 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.\n */\nexport const DEFAULT_VIEW: ViewSettings = {\n grid: true,\n gridLabels: true,\n floor: false,\n environment: 'studio',\n skyColor: WORKBENCH,\n dirLightIntensity: 2.0,\n dirLightFollowCamera: true,\n dirShadowEnabled: true,\n hemiIntensity: 4.0\n}\n"]}
1
+ {"version":3,"file":"figure-view.js","sourceRoot":"","sources":["../../client/modeller/figure-view.ts"],"names":[],"mappings":"AAiEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,WAAW;IACX,SAAS;IACT,QAAQ;IACR,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;CACC,CAAA;AAGV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,IAAkB;IAC1C,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IAClD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAA;AAEvE,qCAAqC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,SAAS,EAAE,OAAO;IAClB,iDAAiD;IACjD,aAAa,EAAE,IAAI;IACnB,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,KAAK;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,SAAS;IACnB,iBAAiB,EAAE,GAAG;IACtB,oBAAoB,EAAE,IAAI;IAC1B,gBAAgB,EAAE,IAAI;IACtB,aAAa,EAAE,GAAG;CACnB,CAAA","sourcesContent":["/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 저장되지 않는다\n *\n * 이것은 자산이 아니라 **작업대**다. 조명을 밝게 하고 바닥을 켠 것은 그 사람이 지금\n * 보기 편한 상태일 뿐, 그 자산의 성질이 아니다. 그래서 `FigureSource` 에 안 들어간다 —\n * `toFigureSource` 는 판에서 `width` · `height` · `depth` 와 몇 가지만 집어 가고 나머지는\n * 버린다.\n *\n * 다른 사람이 같은 자산을 열면 자기 설정으로 본다. 그것이 맞다.\n *\n * ## 씬이 아는 이름을 그대로 쓴다\n *\n * 여기 이름은 전부 `model-layer` 의 상태 이름이다. 옮겨 적는 표를 두지 않는다 — 표를\n * 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n *\n * 값이 바뀌면 씬이 스스로 반영한다(`three-capability` 의 `onchange`). 다만 `grid` 는\n * 한동안 3D 를 세울 때만 읽혔다 — things-scene 10.1.5 에서 고쳤다.\n */\nexport interface ViewSettings {\n /**\n * 어떤 보드에 놓인 것으로 보나 — 씬 전체의 좌표 모드.\n *\n * **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에\n * 매달리나」이고, 이것은 「지금 보고 있는 보드가 어느 쪽을 기준으로 재나」다. 바닥\n * 기준 보드에 천정 기반 OHT 를 놓으면 보드는 안 뒤집히고 OHT 가 천장 높이에 매달린다.\n *\n * 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라\n * 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 보드에서는 성립할 수가\n * 없고(보드에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔\n * 높이가 전부 다른 숫자로 보였다.\n *\n * 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`\n * 천장 기준. **미리보기에만 먹인다** — 편집 캔버스의 `zPos` 는 저작자가 직접 적는\n * 숫자라 그 뜻이 보기 설정에 따라 바뀌면 안 된다.\n */\n placement: 'floor' | 'space' | 'inverted'\n /**\n * 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.\n *\n * 도형은 제가 놓일 방의 높이를 모른다. 보드는 알지만 미리보기에는 보드가 없으므로,\n * 저작자가 가정할 값을 여기서 정한다.\n */\n ceilingHeight: number\n /** 좌표 바탕(축 가이드). 원점과 눈금을 보인다. */\n grid: boolean\n /** 축 이름표(X · Y · Z). 눈금만 두고 글자를 끌 수 있다. */\n gridLabels: boolean\n /** 판의 바닥면. 좌표 바탕과 겹치므로 기본은 끈다. */\n floor: boolean\n /** Environment — 반사(IBL). 금속 재질이 무엇을 비출지 정한다. */\n environment: EnvironmentName\n /** Sky Color — 배경색. */\n skyColor: string\n /** Key Light Intensity. */\n dirLightIntensity: number\n /** Key Light — Follow Camera. 끄면 그림자 방향이 고정된다. */\n dirLightFollowCamera: boolean\n /** Key Light — Cast Shadow. */\n dirShadowEnabled: boolean\n /** Hemisphere Light Intensity — 그늘진 면을 들어올린다. */\n hemiIntensity: number\n}\n\n/**\n * 환경 프리셋 — **`scene-manager` 의 `SkyPreset` 과 같은 여덟이다.**\n *\n * 한동안 다섯만 적었다. 씬은 여덟을 다 받는데 이쪽이 셋을 몰라서 고를 수 없었다.\n * operato-board 의 3D 씬 속성 패널도 같은 여덟을 보인다.\n */\nexport const ENVIRONMENTS = [\n 'studio',\n 'warehouse',\n 'factory',\n 'office',\n 'home',\n 'sunny',\n 'cloudy',\n 'rainy'\n] as const\nexport type EnvironmentName = (typeof ENVIRONMENTS)[number]\n\n/**\n * 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**\n *\n * 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.\n * 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),\n * `ceilingHeight` 는 씬이 모르는 이름이다.\n *\n * **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안\n * `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을\n * 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로\n * 그 길로 캔버스에 들어갈 뻔했다.\n */\nexport function sceneLook(view: ViewSettings): Record<string, unknown> {\n const { placement, ceilingHeight, ...look } = view\n return look\n}\n\n/**\n * 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**\n *\n * 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지\n * 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준\n * (inverted)」.\n */\nexport const SCENE_REFERENCES = ['floor', 'space', 'inverted'] as const\n\n/** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */\nexport const WORKBENCH = '#7c7c7c'\n\n/**\n * 처음 값.\n *\n * **지금 저작 화면이 실제로 쓰던 값 그대로다.** 설정을 만들면서 화면이 달라지면\n * 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.\n */\nexport const DEFAULT_VIEW: ViewSettings = {\n placement: 'floor',\n /* 공장 천장의 흔한 높이. 반도체 fab 의 OHT 레일이 대체로 이 언저리다. */\n ceilingHeight: 4000,\n grid: true,\n gridLabels: true,\n floor: false,\n environment: 'studio',\n skyColor: WORKBENCH,\n dirLightIntensity: 2.0,\n dirLightFollowCamera: true,\n dirShadowEnabled: true,\n hemiIntensity: 4.0\n}\n"]}
@@ -1,34 +1,34 @@
1
1
  import type { PrimitiveKind } from '@hatiolab/figure-model';
2
- import type { BoardModel, PartModel, PlaneAxis } from './figure-source.js';
3
- /** 이 보드에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
4
- export declare function partLimit(board: BoardModel): number;
2
+ import type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js';
3
+ /** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
4
+ export declare function partLimit(draft: FigureDraft): number;
5
5
  /**
6
6
  * 새 부품 하나를 만든다.
7
7
  *
8
- * 보드의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —
8
+ * 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —
9
9
  * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
10
10
  */
11
- export declare function newPart(board: BoardModel, parts: PartModel[], primitive: PrimitiveKind): PartModel;
12
- export declare function addPart(board: BoardModel, parts: PartModel[], primitive: PrimitiveKind): PartModel[];
11
+ export declare function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel;
12
+ export declare function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[];
13
13
  export declare function removePart(parts: PartModel[], index: number): PartModel[];
14
14
  /** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */
15
15
  export declare function duplicatePart(parts: PartModel[], index: number): PartModel[];
16
16
  /** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */
17
17
  export declare function movePart(parts: PartModel[], index: number, to: number): PartModel[];
18
18
  export declare function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[];
19
- /** 부품을 보드의 해당 축 가운데로 옮긴다. */
20
- export declare function centerPart(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
21
- /** 부품을 보드 바닥(또는 해당 축의 시작)에 붙인다. */
19
+ /** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */
20
+ export declare function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
21
+ /** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */
22
22
  export declare function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
23
- /** 부품을 보드의 끝에 붙인다. */
24
- export declare function alignPartToEnd(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
23
+ /** 부품을 상자의 끝에 붙인다. */
24
+ export declare function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
25
25
  /**
26
26
  * 부품을 축 기준으로 대칭 복제한다.
27
27
  *
28
28
  * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수
29
29
  * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.
30
30
  */
31
- export declare function mirrorPart(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
31
+ export declare function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
32
32
  /**
33
33
  * 대칭이 의미가 있는지.
34
34
  *
@@ -36,14 +36,14 @@ export declare function mirrorPart(board: BoardModel, parts: PartModel[], index:
36
36
  * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.
37
37
  * 만들어 놓고 알리는 대신 미리 막는다.
38
38
  */
39
- export declare function canMirror(board: BoardModel, parts: PartModel[], index: number, axis: PlaneAxis): boolean;
39
+ export declare function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean;
40
40
  /**
41
41
  * 부품의 위치와 크기를 격자에 맞춘다.
42
42
  *
43
- * 격자 간격은 보드 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
43
+ * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
44
44
  * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.
45
45
  */
46
- export declare function snapPart(board: BoardModel, parts: PartModel[], index: number): PartModel[];
46
+ export declare function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[];
47
47
  /**
48
48
  * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.
49
49
  *
@@ -51,7 +51,7 @@ export declare function snapPart(board: BoardModel, parts: PartModel[], index: n
51
51
  * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가
52
52
  * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.
53
53
  */
54
- export declare function gridStep(board: BoardModel): number;
54
+ export declare function gridStep(draft: FigureDraft): number;
55
55
  /**
56
56
  * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.
57
57
  *