@things-factory/figure-ui 10.1.4 → 10.1.6

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 (79) hide show
  1. package/client/graphql/index.ts +122 -15
  2. package/client/modeller/figure-ask.ts +5 -5
  3. package/client/modeller/figure-canvas.ts +1 -1
  4. package/client/modeller/figure-capabilities.ts +372 -0
  5. package/client/modeller/figure-history-panel.ts +477 -0
  6. package/client/modeller/figure-inspector.ts +392 -76
  7. package/client/modeller/figure-parts.ts +15 -15
  8. package/client/modeller/figure-preview.ts +2 -2
  9. package/client/modeller/figure-report.ts +102 -37
  10. package/client/modeller/figure-settings.ts +20 -20
  11. package/client/modeller/figure-side.ts +54 -8
  12. package/client/modeller/figure-source.test.ts +15 -1
  13. package/client/modeller/figure-source.ts +31 -15
  14. package/client/modeller/figure-thumbnail.ts +49 -4
  15. package/client/modeller/maturity.ts +10 -10
  16. package/client/pages/figure-list-page.ts +450 -227
  17. package/client/pages/figure-modeller-page.ts +282 -43
  18. package/client/types.ts +42 -0
  19. package/client/viewparts/figure-thumb.ts +172 -0
  20. package/dist-client/graphql/index.d.ts +31 -4
  21. package/dist-client/graphql/index.js +102 -12
  22. package/dist-client/graphql/index.js.map +1 -1
  23. package/dist-client/modeller/figure-ask.js +5 -5
  24. package/dist-client/modeller/figure-ask.js.map +1 -1
  25. package/dist-client/modeller/figure-canvas.js +1 -1
  26. package/dist-client/modeller/figure-canvas.js.map +1 -1
  27. package/dist-client/modeller/figure-capabilities.d.ts +42 -0
  28. package/dist-client/modeller/figure-capabilities.js +344 -0
  29. package/dist-client/modeller/figure-capabilities.js.map +1 -0
  30. package/dist-client/modeller/figure-history-panel.d.ts +56 -0
  31. package/dist-client/modeller/figure-history-panel.js +490 -0
  32. package/dist-client/modeller/figure-history-panel.js.map +1 -0
  33. package/dist-client/modeller/figure-inspector.d.ts +84 -0
  34. package/dist-client/modeller/figure-inspector.js +379 -75
  35. package/dist-client/modeller/figure-inspector.js.map +1 -1
  36. package/dist-client/modeller/figure-parts.js +15 -15
  37. package/dist-client/modeller/figure-parts.js.map +1 -1
  38. package/dist-client/modeller/figure-preview.js +2 -2
  39. package/dist-client/modeller/figure-preview.js.map +1 -1
  40. package/dist-client/modeller/figure-report.d.ts +18 -1
  41. package/dist-client/modeller/figure-report.js +105 -37
  42. package/dist-client/modeller/figure-report.js.map +1 -1
  43. package/dist-client/modeller/figure-settings.js +19 -19
  44. package/dist-client/modeller/figure-settings.js.map +1 -1
  45. package/dist-client/modeller/figure-side.d.ts +7 -0
  46. package/dist-client/modeller/figure-side.js +66 -7
  47. package/dist-client/modeller/figure-side.js.map +1 -1
  48. package/dist-client/modeller/figure-source.d.ts +15 -8
  49. package/dist-client/modeller/figure-source.js +16 -7
  50. package/dist-client/modeller/figure-source.js.map +1 -1
  51. package/dist-client/modeller/figure-thumbnail.js +40 -4
  52. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  53. package/dist-client/modeller/maturity.js +10 -10
  54. package/dist-client/modeller/maturity.js.map +1 -1
  55. package/dist-client/pages/figure-list-page.d.ts +45 -25
  56. package/dist-client/pages/figure-list-page.js +439 -247
  57. package/dist-client/pages/figure-list-page.js.map +1 -1
  58. package/dist-client/pages/figure-modeller-page.d.ts +54 -0
  59. package/dist-client/pages/figure-modeller-page.js +279 -43
  60. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  61. package/dist-client/tsconfig.tsbuildinfo +1 -1
  62. package/dist-client/types.d.ts +42 -0
  63. package/dist-client/types.js.map +1 -1
  64. package/dist-client/viewparts/figure-thumb.d.ts +29 -0
  65. package/dist-client/viewparts/figure-thumb.js +168 -0
  66. package/dist-client/viewparts/figure-thumb.js.map +1 -0
  67. package/dist-server/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +9 -5
  69. package/test/i18n-prefix-guard.test.ts +129 -0
  70. package/test/korean-register-guard.test.ts +83 -0
  71. package/translations/en.json +307 -249
  72. package/translations/ja.json +275 -249
  73. package/translations/ko.json +307 -249
  74. package/translations/ms.json +275 -249
  75. package/translations/zh.json +275 -249
  76. package/client/viewparts/figure-card.ts +0 -226
  77. package/dist-client/viewparts/figure-card.d.ts +0 -24
  78. package/dist-client/viewparts/figure-card.js +0 -221
  79. package/dist-client/viewparts/figure-card.js.map +0 -1
@@ -149,7 +149,6 @@ const FULL: FigureSource = {
149
149
  base: { x: 3000, y: 800, z: 2000 },
150
150
  detailLevel: 'L',
151
151
  styleKit: 'factory',
152
- anchor: { x: 0, y: -400, z: 0 },
153
152
  parts: [
154
153
  {
155
154
  name: 'deck',
@@ -200,8 +199,23 @@ const FULL: FigureSource = {
200
199
  material: { token: 'palette.neutral' },
201
200
  sizing: 'repeat',
202
201
  repeat: { axis: 'x', pitch: 200 }
202
+ },
203
+ {
204
+ /*
205
+ 물건을 놓는 자리이면서 드나드는 문이다 — AGV 의 상판이 그렇다. **그려지지 않으므로**
206
+ 재질이 없다.
207
+
208
+ 그리지 않는 것이라 화면으로는 왕복에서 누락됐는지 알 수 없다. 자리를 잃어버린 도형은
209
+ 멀쩡해 보이면서 아무것도 받지 못한다.
210
+ */
211
+ name: 'seat',
212
+ primitive: 'cube',
213
+ transform: { position: { x: 0, y: 420, z: 0 }, size: { x: 700, y: 500, z: 500 } },
214
+ capability: { roles: ['slot', 'port-in', 'port-out'], accepts: ['PALLET'], capacity: 2 }
203
215
  }
204
216
  ],
217
+ /* 이 도형이 지는 능력. 부품이 아니라 도형 전체의 것이다. */
218
+ capabilities: ['FlowNode'],
205
219
  /*
206
220
  움직임. 편집면이 아직 안 만지는 자리이며, **바로 그래서 여기 있다.**
207
221
 
@@ -82,14 +82,6 @@ export interface BoardModel {
82
82
  figureType?: string
83
83
  detailLevel?: DetailLevel
84
84
  styleKit?: string
85
- /**
86
- * 형상 전체의 기준점.
87
- *
88
- * **부품의 `anchor` 와 다른 것이다.** 이쪽은 점(`Vec3`)이고 저쪽은 축별로 어느 면을
89
- * 붙잡느냐다. 한 형식 안에 같은 낱말이 둘인 것은 좋지 않으나, 이름을 바꾸면 저장된
90
- * 원본이 따라와야 하므로 여기서 정하지 않는다.
91
- */
92
- anchor?: Vec3
93
85
  /**
94
86
  * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.
95
87
  *
@@ -97,6 +89,14 @@ export interface BoardModel {
97
89
  * 저장하는 것만으로 **조용히 지워지기** 때문이다.
98
90
  */
99
91
  animations?: FigureSource['animations']
92
+ /**
93
+ * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.
94
+ *
95
+ * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가
96
+ * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐
97
+ * 주나**를 말하는 다른 칸이다.
98
+ */
99
+ capabilities?: FigureSource['capabilities']
100
100
  }
101
101
 
102
102
  /** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */
@@ -159,6 +159,13 @@ export interface PartModel {
159
159
  anchor?: FigurePart['anchor']
160
160
  repeat?: FigurePart['repeat']
161
161
  label?: FigurePart['label']
162
+ /**
163
+ * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.
164
+ *
165
+ * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,
166
+ * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.
167
+ */
168
+ capability?: FigurePart['capability']
162
169
  }
163
170
 
164
171
  const RAD_TO_DEG = 180 / Math.PI
@@ -223,11 +230,11 @@ export function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSou
223
230
  return withoutEmpty({
224
231
  type: board.figureType ?? '',
225
232
  base: { x: board.width, y: board.depth, z: board.height },
226
- anchor: board.anchor,
227
233
  detailLevel: board.detailLevel,
228
234
  styleKit: board.styleKit,
229
235
  parts: parts.map(part => partTo(part, half)),
230
- animations: board.animations
236
+ animations: board.animations,
237
+ capabilities: board.capabilities
231
238
  }) as FigureSource
232
239
  }
233
240
 
@@ -239,6 +246,13 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
239
246
  })
240
247
 
241
248
  const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })
249
+ /*
250
+ 재질이 하나도 없으면 **칸 자체를 안 적는다.**
251
+
252
+ 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어
253
+ `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이
254
+ 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.
255
+ */
242
256
  const material = withoutEmpty({
243
257
  token: part.token,
244
258
  preset: part.preset,
@@ -262,13 +276,14 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
262
276
  }),
263
277
  segments: part.segments,
264
278
  shape: Object.keys(shape).length > 0 ? shape : undefined,
265
- material,
279
+ material: Object.keys(material).length > 0 ? material : undefined,
266
280
  materialSlot: part.materialSlot,
267
281
  keepRound: part.keepRound,
268
282
  sizing: part.sizing,
269
283
  anchor: part.anchor,
270
284
  repeat: part.repeat,
271
- label: part.label
285
+ label: part.label,
286
+ capability: part.capability
272
287
  }) as FigurePart
273
288
  }
274
289
 
@@ -283,8 +298,8 @@ export function fromFigureSource(source: FigureSource): { board: BoardModel; par
283
298
  figureType: source.type,
284
299
  detailLevel: source.detailLevel,
285
300
  styleKit: source.styleKit,
286
- anchor: source.anchor,
287
- animations: source.animations
301
+ animations: source.animations,
302
+ capabilities: source.capabilities
288
303
  }) as BoardModel
289
304
 
290
305
  return { board, parts: source.parts.map(part => partFrom(part, half)) }
@@ -333,6 +348,7 @@ function partFrom(part: FigurePart, half: Vec3): PartModel {
333
348
  sizing: part.sizing,
334
349
  anchor: part.anchor,
335
350
  repeat: part.repeat,
336
- label: part.label
351
+ label: part.label,
352
+ capability: part.capability
337
353
  }) as PartModel
338
354
  }
@@ -34,11 +34,40 @@ interface Shooter extends HTMLElement {
34
34
  snapshot?: (width?: number, height?: number) => Promise<string | undefined>
35
35
  }
36
36
 
37
- /** 프레임 두 장 뒤에 한 번 더 쉰다 — 씬이 서고 카메라가 자리를 잡을 틈. 씬에 그 신호가 없다. */
37
+ /**
38
+ * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.
39
+ *
40
+ * **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면
41
+ * 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,
42
+ * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.
43
+ *
44
+ * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래
45
+ * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**
46
+ */
38
47
  function settle(): Promise<void> {
39
- return new Promise(resolve =>
48
+ const frames = new Promise<void>(resolve =>
40
49
  requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))
41
50
  )
51
+
52
+ return Promise.race([frames, delay(600)])
53
+ }
54
+
55
+ function delay(ms: number): Promise<void> {
56
+ return new Promise(resolve => setTimeout(resolve, ms))
57
+ }
58
+
59
+ /**
60
+ * 그림을 기다리는 시간의 한도.
61
+ *
62
+ * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가
63
+ * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,
64
+ * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).
65
+ */
66
+ const PATIENCE = 5000
67
+
68
+ /** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */
69
+ function within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {
70
+ return Promise.race([work, delay(ms).then(() => undefined)])
42
71
  }
43
72
 
44
73
  export async function captureThumbnail(
@@ -66,9 +95,25 @@ export async function captureThumbnail(
66
95
 
67
96
  try {
68
97
  host.appendChild(shooter)
69
- await settle()
70
98
 
71
- return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined
99
+ /*
100
+ **한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을
101
+ 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속
102
+ 안 생기면 그 줄이 단서가 된다.
103
+ */
104
+ const shot = await within(
105
+ (async () => {
106
+ await settle()
107
+ return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined
108
+ })(),
109
+ PATIENCE
110
+ )
111
+
112
+ if (!shot) {
113
+ console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)
114
+ }
115
+
116
+ return shot
72
117
  } catch (e) {
73
118
  console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)
74
119
  return undefined
@@ -70,7 +70,7 @@ export function maturityOf(source: FigureSource, cost: FigureCost, score: Figure
70
70
  function scale(cost: FigureCost): MaturityAxis {
71
71
  const capacity = capacityFrom(cost.groups, cost.triangles)
72
72
  if (!capacity) {
73
- return { key: 'scale', blocked: 'text.maturity-blocked-no-parts' }
73
+ return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' }
74
74
  }
75
75
 
76
76
  return {
@@ -79,7 +79,7 @@ function scale(cost: FigureCost): MaturityAxis {
79
79
  // 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.
80
80
  ratio: Math.min(1, capacity.instances / BOARD_ASSETS),
81
81
  reading: String(capacity.instances),
82
- because: 'text.maturity-scale-because'
82
+ because: 'figure.text.maturity-scale-because'
83
83
  }
84
84
  }
85
85
 
@@ -102,8 +102,8 @@ function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
102
102
  return {
103
103
  key: 'consistency',
104
104
  ratio: (3 - faults) / 3,
105
- readingKey: faults === 0 ? 'text.maturity-consistency-clean' : 'text.maturity-consistency-faults',
106
- because: 'text.maturity-consistency-because'
105
+ readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',
106
+ because: 'figure.text.maturity-consistency-because'
107
107
  }
108
108
  }
109
109
 
@@ -117,7 +117,7 @@ function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
117
117
  function reuse(score: FigureScore): MaturityAxis {
118
118
  const { distinct, parts, saved } = score.reuse
119
119
  if (parts === 0) {
120
- return { key: 'reuse', blocked: 'text.maturity-blocked-no-parts' }
120
+ return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' }
121
121
  }
122
122
 
123
123
  return {
@@ -125,7 +125,7 @@ function reuse(score: FigureScore): MaturityAxis {
125
125
  // ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다
126
126
  ratio: 1 - distinct / parts,
127
127
  reading: String(saved),
128
- because: 'text.maturity-reuse-because'
128
+ because: 'figure.text.maturity-reuse-because'
129
129
  }
130
130
  }
131
131
 
@@ -148,7 +148,7 @@ function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
148
148
  // 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.
149
149
  ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),
150
150
  reading: `${cost.parts} / ${limit}`,
151
- because: 'text.maturity-detail-because'
151
+ because: 'figure.text.maturity-detail-because'
152
152
  }
153
153
  }
154
154
 
@@ -162,7 +162,7 @@ function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
162
162
  * **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.
163
163
  */
164
164
  function reactive(source: FigureSource): MaturityAxis {
165
- return { key: 'reactive', blocked: 'text.maturity-blocked-no-binding' }
165
+ return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' }
166
166
  }
167
167
 
168
168
  /**
@@ -205,7 +205,7 @@ function reactive(source: FigureSource): MaturityAxis {
205
205
  function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
206
206
  const regions = topViewRegions(source)
207
207
  if (regions.total === 0) {
208
- return { key: 'readable', blocked: 'text.maturity-blocked-no-parts' }
208
+ return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }
209
209
  }
210
210
 
211
211
  // 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.
@@ -221,7 +221,7 @@ function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
221
221
  key: 'readable',
222
222
  ratio: Math.min(1, entropy / ceiling),
223
223
  reading: `${regions.byMaterial.size}`,
224
- because: 'text.maturity-readable-because'
224
+ because: 'figure.text.maturity-readable-because'
225
225
  }
226
226
  }
227
227