@things-factory/figure-ui 10.1.75 → 10.1.76

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 (235) hide show
  1. package/client/figure-assistant.ts +3 -3
  2. package/client/graphql/{v3-store.ts → store.ts} +6 -6
  3. package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
  4. package/client/modeller/{v3-align.ts → align.ts} +14 -14
  5. package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
  6. package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
  7. package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
  8. package/client/modeller/{v3-editor.ts → editor.ts} +117 -117
  9. package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
  10. package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
  11. package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
  12. package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
  13. package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
  14. package/client/modeller/figure-history-panel.ts +1 -1
  15. package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
  16. package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
  17. package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
  18. package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
  19. package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
  20. package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
  21. package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
  22. package/client/modeller/gate-findings.ts +2 -2
  23. package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
  24. package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
  25. package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
  26. package/client/modeller/{v3-links.ts → links.ts} +59 -59
  27. package/client/modeller/{v3-live.ts → live.ts} +9 -9
  28. package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
  29. package/client/modeller/modeller-route.ts +1 -1
  30. package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
  31. package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
  32. package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
  33. package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
  34. package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
  35. package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
  36. package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
  37. package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
  38. package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
  39. package/client/pages/figure-list-page.ts +4 -4
  40. package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
  41. package/client/route.ts +1 -5
  42. package/dist-client/figure-assistant.js +3 -3
  43. package/dist-client/figure-assistant.js.map +1 -1
  44. package/dist-client/graphql/store.d.ts +3 -0
  45. package/dist-client/graphql/store.js +39 -0
  46. package/dist-client/graphql/store.js.map +1 -0
  47. package/dist-client/modeller/ai-proposal.d.ts +51 -0
  48. package/dist-client/modeller/ai-proposal.js +64 -0
  49. package/dist-client/modeller/ai-proposal.js.map +1 -0
  50. package/dist-client/modeller/align.d.ts +30 -0
  51. package/dist-client/modeller/align.js +75 -0
  52. package/dist-client/modeller/align.js.map +1 -0
  53. package/dist-client/modeller/canvas.d.ts +72 -0
  54. package/dist-client/modeller/canvas.js +379 -0
  55. package/dist-client/modeller/canvas.js.map +1 -0
  56. package/dist-client/modeller/clips.d.ts +39 -0
  57. package/dist-client/modeller/clips.js +58 -0
  58. package/dist-client/modeller/clips.js.map +1 -0
  59. package/dist-client/modeller/controls.d.ts +28 -0
  60. package/dist-client/modeller/controls.js +28 -0
  61. package/dist-client/modeller/controls.js.map +1 -0
  62. package/dist-client/modeller/editor.d.ts +203 -0
  63. package/dist-client/modeller/editor.js +1526 -0
  64. package/dist-client/modeller/editor.js.map +1 -0
  65. package/dist-client/modeller/fields.d.ts +45 -0
  66. package/dist-client/modeller/fields.js +83 -0
  67. package/dist-client/modeller/fields.js.map +1 -0
  68. package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
  69. package/dist-client/modeller/figure-capability-panel.js +141 -0
  70. package/dist-client/modeller/figure-capability-panel.js.map +1 -0
  71. package/dist-client/modeller/figure-clips.d.ts +31 -0
  72. package/dist-client/modeller/figure-clips.js +268 -0
  73. package/dist-client/modeller/figure-clips.js.map +1 -0
  74. package/dist-client/modeller/figure-controls.d.ts +36 -0
  75. package/dist-client/modeller/figure-controls.js +226 -0
  76. package/dist-client/modeller/figure-controls.js.map +1 -0
  77. package/dist-client/modeller/figure-facts.d.ts +35 -0
  78. package/dist-client/modeller/figure-facts.js +55 -0
  79. package/dist-client/modeller/figure-facts.js.map +1 -0
  80. package/dist-client/modeller/figure-first-save.d.ts +20 -0
  81. package/dist-client/modeller/figure-first-save.js +118 -0
  82. package/dist-client/modeller/figure-first-save.js.map +1 -0
  83. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  84. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  85. package/dist-client/modeller/figure-links.d.ts +77 -0
  86. package/dist-client/modeller/figure-links.js +649 -0
  87. package/dist-client/modeller/figure-links.js.map +1 -0
  88. package/dist-client/modeller/figure-material-panel.d.ts +39 -0
  89. package/dist-client/modeller/figure-material-panel.js +323 -0
  90. package/dist-client/modeller/figure-material-panel.js.map +1 -0
  91. package/dist-client/modeller/figure-parts.d.ts +69 -0
  92. package/dist-client/modeller/figure-parts.js +559 -0
  93. package/dist-client/modeller/figure-parts.js.map +1 -0
  94. package/dist-client/modeller/figure-preview.d.ts +34 -0
  95. package/dist-client/modeller/figure-preview.js +346 -0
  96. package/dist-client/modeller/figure-preview.js.map +1 -0
  97. package/dist-client/modeller/figure-properties.d.ts +45 -0
  98. package/dist-client/modeller/figure-properties.js +239 -0
  99. package/dist-client/modeller/figure-properties.js.map +1 -0
  100. package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
  101. package/dist-client/modeller/figure-recipe-catalog.js +399 -0
  102. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
  103. package/dist-client/modeller/figure-verdict.d.ts +64 -0
  104. package/dist-client/modeller/figure-verdict.js +787 -0
  105. package/dist-client/modeller/figure-verdict.js.map +1 -0
  106. package/dist-client/modeller/first-save.d.ts +17 -0
  107. package/dist-client/modeller/first-save.js +26 -0
  108. package/dist-client/modeller/first-save.js.map +1 -0
  109. package/dist-client/modeller/gate-findings.d.ts +2 -2
  110. package/dist-client/modeller/gate-findings.js +2 -2
  111. package/dist-client/modeller/gate-findings.js.map +1 -1
  112. package/dist-client/modeller/gizmo.d.ts +72 -0
  113. package/dist-client/modeller/gizmo.js +111 -0
  114. package/dist-client/modeller/gizmo.js.map +1 -0
  115. package/dist-client/modeller/help.d.ts +18 -0
  116. package/dist-client/modeller/help.js +37 -0
  117. package/dist-client/modeller/help.js.map +1 -0
  118. package/dist-client/modeller/joint-drag.d.ts +19 -0
  119. package/dist-client/modeller/joint-drag.js +97 -0
  120. package/dist-client/modeller/joint-drag.js.map +1 -0
  121. package/dist-client/modeller/joint-fan.d.ts +9 -0
  122. package/dist-client/modeller/joint-fan.js +276 -0
  123. package/dist-client/modeller/joint-fan.js.map +1 -0
  124. package/dist-client/modeller/keys.d.ts +22 -0
  125. package/dist-client/modeller/keys.js +26 -0
  126. package/dist-client/modeller/keys.js.map +1 -0
  127. package/dist-client/modeller/links.d.ts +267 -0
  128. package/dist-client/modeller/links.js +480 -0
  129. package/dist-client/modeller/links.js.map +1 -0
  130. package/dist-client/modeller/live.d.ts +37 -0
  131. package/dist-client/modeller/live.js +68 -0
  132. package/dist-client/modeller/live.js.map +1 -0
  133. package/dist-client/modeller/maturity.d.ts +120 -0
  134. package/dist-client/modeller/maturity.js +313 -0
  135. package/dist-client/modeller/maturity.js.map +1 -0
  136. package/dist-client/modeller/modeller-route.d.ts +1 -1
  137. package/dist-client/modeller/modeller-route.js +1 -1
  138. package/dist-client/modeller/modeller-route.js.map +1 -1
  139. package/dist-client/modeller/occupancy.d.ts +18 -0
  140. package/dist-client/modeller/occupancy.js +24 -0
  141. package/dist-client/modeller/occupancy.js.map +1 -0
  142. package/dist-client/modeller/palette.d.ts +7 -0
  143. package/dist-client/modeller/palette.js +46 -0
  144. package/dist-client/modeller/palette.js.map +1 -0
  145. package/dist-client/modeller/preview-model.d.ts +47 -0
  146. package/dist-client/modeller/preview-model.js +68 -0
  147. package/dist-client/modeller/preview-model.js.map +1 -0
  148. package/dist-client/modeller/product-session.d.ts +510 -0
  149. package/dist-client/modeller/product-session.js +157 -0
  150. package/dist-client/modeller/product-session.js.map +1 -0
  151. package/dist-client/modeller/recipe-source.d.ts +8 -0
  152. package/dist-client/modeller/recipe-source.js +14 -0
  153. package/dist-client/modeller/recipe-source.js.map +1 -0
  154. package/dist-client/modeller/recipes.d.ts +37 -0
  155. package/dist-client/modeller/recipes.js +103 -0
  156. package/dist-client/modeller/recipes.js.map +1 -0
  157. package/dist-client/modeller/refusals.d.ts +68 -0
  158. package/dist-client/modeller/refusals.js +142 -0
  159. package/dist-client/modeller/refusals.js.map +1 -0
  160. package/dist-client/modeller/renderer.d.ts +40 -0
  161. package/dist-client/modeller/renderer.js +73 -0
  162. package/dist-client/modeller/renderer.js.map +1 -0
  163. package/dist-client/modeller/stage-model.d.ts +39 -0
  164. package/dist-client/modeller/stage-model.js +147 -0
  165. package/dist-client/modeller/stage-model.js.map +1 -0
  166. package/dist-client/modeller/thumbnail.d.ts +3 -0
  167. package/dist-client/modeller/thumbnail.js +75 -0
  168. package/dist-client/modeller/thumbnail.js.map +1 -0
  169. package/dist-client/pages/figure-list-page.d.ts +1 -1
  170. package/dist-client/pages/figure-list-page.js +4 -4
  171. package/dist-client/pages/figure-list-page.js.map +1 -1
  172. package/dist-client/pages/figure-modeller-page.d.ts +102 -0
  173. package/dist-client/pages/figure-modeller-page.js +548 -0
  174. package/dist-client/pages/figure-modeller-page.js.map +1 -0
  175. package/dist-client/route.d.ts +1 -1
  176. package/dist-client/route.js +1 -5
  177. package/dist-client/route.js.map +1 -1
  178. package/dist-client/tsconfig.tsbuildinfo +1 -1
  179. package/docs/product-cutover.md +25 -0
  180. package/package.json +5 -5
  181. package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
  182. package/test/{v3-align.test.ts → align.test.ts} +8 -8
  183. package/test/browser/modeller-page.js +30 -30
  184. package/test/browser/modeller.check.mjs +14 -14
  185. package/test/browser/serve.mjs +1 -1
  186. package/test/browser/thumbnail-page.js +6 -6
  187. package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
  188. package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
  189. package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
  190. package/test/figure-assistant-surface.test.ts +3 -3
  191. package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
  192. package/test/fixtures/joint-column.json +2 -2
  193. package/test/fixtures/joint-roller.json +2 -2
  194. package/test/fixtures/robot-arm-6dof.json +2 -2
  195. package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
  196. package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
  197. package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
  198. package/test/joints.ts +6 -6
  199. package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
  200. package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
  201. package/test/{v3-links.test.ts → links.test.ts} +201 -201
  202. package/test/{v3-live.test.ts → live.test.ts} +18 -18
  203. package/test/{v3-material.test.ts → material.test.ts} +12 -12
  204. package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
  205. package/test/modeller-route.test.ts +5 -6
  206. package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
  207. package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +5 -5
  208. package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
  209. package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
  210. package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
  211. package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
  212. package/test/{v3-recipes.test.ts → recipes.test.ts} +9 -9
  213. package/test/{v3-refusals.test.ts → refusals.test.ts} +43 -43
  214. package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
  215. package/test/renderer.test.ts +24 -0
  216. package/test/resolve-ts-siblings.mjs +1 -1
  217. package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
  218. package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
  219. package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
  220. package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
  221. package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
  222. package/test/{v3-view.test.ts → view.test.ts} +2 -2
  223. package/things-factory.config.js +2 -3
  224. package/translations/en.json +2 -2
  225. package/translations/ja.json +2 -2
  226. package/translations/ko.json +2 -2
  227. package/translations/ms.json +2 -2
  228. package/translations/zh.json +2 -2
  229. package/docs/v3-product-cutover.md +0 -25
  230. package/test/v3-renderer.test.ts +0 -24
  231. /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
  232. /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
  233. /package/client/modeller/{v3-help.ts → help.ts} +0 -0
  234. /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
  235. /package/client/modeller/{v3-refusals.ts → refusals.ts} +0 -0
@@ -0,0 +1,147 @@
1
+ import * as THREE from 'three';
2
+ import { buildFigureGeometry } from '@hatiolab/things-scene';
3
+ const round = (n) => Math.round(n * 1e6) / 1e6;
4
+ export function frameOf(part) {
5
+ /* The box things-scene's `figure-part` centres the body on: the same builder, measured the same way. */
6
+ let built;
7
+ try {
8
+ built = buildFigureGeometry(part.shape, part.segments);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ if (!built?.getAttribute('position'))
14
+ return null;
15
+ built.computeBoundingBox();
16
+ const box = built.boundingBox;
17
+ const unit = { centre: box.getCenter(new THREE.Vector3()), extent: box.getSize(new THREE.Vector3()) };
18
+ built.dispose();
19
+ const r = part.pose.r;
20
+ const rotation = new THREE.Matrix4().set(r[0][0], r[0][1], r[0][2], 0, r[1][0], r[1][1], r[1][2], 0, r[2][0], r[2][1], r[2][2], 0, 0, 0, 0, 1);
21
+ return { centre: unit.centre, extent: unit.extent, rotation, origin: new THREE.Vector3(...part.pose.t) };
22
+ }
23
+ /** The colours a body is drawn in, from the palette the modeller resolved (the asset's, over which the theme wins). */
24
+ function colours(part, palette) {
25
+ const m = part.material;
26
+ const glow = m.emissive?.on ? { color: palette[m.emissive.token] ?? palette[m.token] ?? '#9a9a9a', intensity: m.emissive.intensity } : undefined;
27
+ return { color: palette[m.token] ?? '#9a9a9a', preset: m.preset, transparent: !!m.transparent, ...(glow ? { glow } : {}) };
28
+ }
29
+ /** The component state that draws a body where figure-model put it. */
30
+ export function componentOf(part, palette, board) {
31
+ const frame = frameOf(part);
32
+ if (!frame)
33
+ return null;
34
+ const { centre, extent, rotation, origin } = frame;
35
+ const world = centre.clone().applyMatrix4(rotation).add(origin);
36
+ const euler = new THREE.Euler().setFromRotationMatrix(rotation, 'XYZ');
37
+ const width = extent.x || 1, depth = extent.y || 1, height = extent.z || 1;
38
+ return {
39
+ type: 'figure-part',
40
+ name: part.id,
41
+ shape: part.shape,
42
+ ...(part.segments !== undefined ? { segments: part.segments } : {}),
43
+ ...colours(part, palette),
44
+ width: round(width),
45
+ depth: round(depth),
46
+ height: round(height),
47
+ left: round(world.x + board.width / 2 - width / 2),
48
+ top: round(world.z + board.height / 2 - height / 2),
49
+ zPos: round(world.y - depth / 2),
50
+ rotationX: round(euler.x),
51
+ rotation: round(-euler.y),
52
+ rotationY: round(euler.z)
53
+ };
54
+ }
55
+ const num = (v, fallback = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
56
+ /**
57
+ * What the gizmo did, in the inputs' terms: where the body's origin is now, how it is turned (degrees, rigid@1's
58
+ * order) and how much each of its own axes grew. Only what changed is given; a component the gizmo did not move gives
59
+ * nothing.
60
+ */
61
+ export function gizmoResultOf(part, after, board) {
62
+ const frame = frameOf(part);
63
+ if (!frame)
64
+ return {};
65
+ const { centre, extent, rotation, origin } = frame;
66
+ const width = num(after.width, extent.x), depth = num(after.depth, extent.y), height = num(after.height, extent.z);
67
+ const scale = new THREE.Vector3(width / (extent.x || 1), depth / (extent.y || 1), height / (extent.z || 1));
68
+ const world = new THREE.Vector3(num(after.left) + width / 2 - board.width / 2, num(after.zPos) + depth / 2, num(after.top) + height / 2 - board.height / 2);
69
+ const euler = new THREE.Euler(num(after.rotationX), -num(after.rotation), num(after.rotationY), 'XYZ');
70
+ const turnedTo = new THREE.Matrix4().makeRotationFromEuler(euler);
71
+ const before = new THREE.Quaternion().setFromRotationMatrix(rotation);
72
+ const now = new THREE.Quaternion().setFromEuler(euler);
73
+ const turned = before.angleTo(now) > 1e-4;
74
+ const sized = [scale.x, scale.y, scale.z].some(s => Math.abs(s - 1) > 1e-4);
75
+ /* The origin that puts the (scaled, turned) box centre where the scene has it. */
76
+ const newOrigin = world.clone().sub(centre.clone().multiply(scale).applyMatrix4(turnedTo));
77
+ const moved = newOrigin.distanceTo(origin) > 0.5;
78
+ const result = {};
79
+ if (moved || turned)
80
+ result.position = newOrigin.toArray();
81
+ if (turned)
82
+ result.rotation = [euler.x, euler.y, euler.z].map(THREE.MathUtils.radToDeg);
83
+ if (sized)
84
+ result.scale = scale.toArray();
85
+ return result;
86
+ }
87
+ /**
88
+ * A drag as amounts, for a part whose place is worked out (`nudge-part`): how far its origin moved and how far each of
89
+ * its angles turned (rigid@1's order, degrees). Nothing on an axis that did not change.
90
+ */
91
+ export function nudgeOf(part, result) {
92
+ const axes = ['x', 'y', 'z'];
93
+ const by = {};
94
+ const turn = {};
95
+ if (result.position)
96
+ axes.forEach((axis, i) => {
97
+ const d = round(result.position[i] - part.pose.t[i]);
98
+ if (Math.abs(d) > 1e-6)
99
+ by[axis] = d;
100
+ });
101
+ if (result.rotation) {
102
+ const r = part.pose.r;
103
+ const now = new THREE.Euler().setFromRotationMatrix(new THREE.Matrix4().set(r[0][0], r[0][1], r[0][2], 0, r[1][0], r[1][1], r[1][2], 0, r[2][0], r[2][1], r[2][2], 0, 0, 0, 0, 1), 'XYZ');
104
+ const was = [now.x, now.y, now.z].map(radians => (radians * 180) / Math.PI);
105
+ axes.forEach((axis, i) => {
106
+ const d = round(result.rotation[i] - was[i]);
107
+ if (Math.abs(d) > 1e-6)
108
+ turn[axis] = d;
109
+ });
110
+ }
111
+ return { by, turn };
112
+ }
113
+ /** The keys of `next` whose values differ from `current` -- only those are set, so the scene raises no echo. */
114
+ export function changedKeys(current, next) {
115
+ const diff = {};
116
+ for (const [key, value] of Object.entries(next)) {
117
+ if (key === 'type')
118
+ continue;
119
+ const was = current[key];
120
+ const same = typeof value === 'object' && value !== null ? JSON.stringify(was) === JSON.stringify(value) : was === value;
121
+ if (!same)
122
+ diff[key] = value;
123
+ }
124
+ return Object.keys(diff).length ? diff : null;
125
+ }
126
+ /** A board wide and deep enough for the figure, with room around it; its size only grows while a figure is edited. */
127
+ export function boardFor(parts, at = { width: 0, height: 0 }) {
128
+ const box = new THREE.Box3();
129
+ for (const part of parts) {
130
+ const frame = frameOf(part);
131
+ if (!frame)
132
+ continue;
133
+ const half = frame.extent.clone().multiplyScalar(0.5);
134
+ const corners = [-1, 1].flatMap(x => [-1, 1].flatMap(y => [-1, 1].map(z => new THREE.Vector3(x * half.x, y * half.y, z * half.z))));
135
+ for (const corner of corners)
136
+ box.expandByPoint(corner.add(frame.centre).applyMatrix4(frame.rotation).add(frame.origin));
137
+ }
138
+ const size = box.isEmpty() ? new THREE.Vector3(1000, 1000, 1000) : box.getSize(new THREE.Vector3());
139
+ const reach = (axis) => 2 * Math.max(Math.abs(box.isEmpty() ? 500 : box.min[axis]), Math.abs(box.isEmpty() ? 500 : box.max[axis]));
140
+ return {
141
+ width: Math.max(at.width, Math.ceil(reach('x') * 1.2), 200),
142
+ height: Math.max(at.height, Math.ceil(reach('z') * 1.2), 200),
143
+ /* Only grows, as the plan does: a raised arm made the board taller than it was, and a new board lost the camera. */
144
+ depth: Math.max(at.depth ?? 0, Math.ceil(size.y * 1.1), 200)
145
+ };
146
+ }
147
+ //# sourceMappingURL=stage-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-model.js","sourceRoot":"","sources":["../../client/modeller/stage-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAsC5D,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;AAEtD,MAAM,UAAU,OAAO,CAAC,IAAsB;IAC5C,wGAAwG;IACxG,IAAI,KAAuC,CAAA;IAC3C,IAAI,CAAC;QACH,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,KAAc,EAAE,IAAI,CAAC,QAAQ,CAAqC,CAAA;IACrG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IACjD,KAAK,CAAC,kBAAkB,EAAE,CAAA;IAC1B,MAAM,GAAG,GAAG,KAAK,CAAC,WAAY,CAAA;IAC9B,MAAM,IAAI,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,CAAA;IACrG,KAAK,CAAC,OAAO,EAAE,CAAA;IACf,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACrB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAChK,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAI,IAAI,CAAC,IAAI,CAAC,CAA8B,CAAC,EAAE,CAAA;AACxI,CAAC;AAED,uHAAuH;AACvH,SAAS,OAAO,CAAC,IAAsB,EAAE,OAA+B;IACtE,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAA;IACvB,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAChJ,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AAC5H,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,IAAsB,EAAE,OAA+B,EAAE,KAAY;IAC/F,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAClD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,qBAAqB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,EACzB,KAAK,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,EACrB,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAA;IACxB,OAAO;QACL,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,IAAI,CAAC,EAAE;QACb,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,GAAG,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;QACzB,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;QACnB,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;QACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;QAClD,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;QACnD,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;QAChC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACzB,QAAQ,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACzB,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;KAC1B,CAAA;AACH,CAAC;AAED,MAAM,GAAG,GAAG,CAAC,CAAU,EAAE,QAAQ,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;AAEtG;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAsB,EAAE,KAA8B,EAAE,KAAY;IAChG,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IACrB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAClD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EACtC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAClC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3G,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC3J,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAA;IACtG,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAA;IAEjE,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IACrE,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA;IACzC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAC3E,kFAAkF;IAClF,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC1F,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,GAAG,CAAA;IAEhD,MAAM,MAAM,GAAgB,EAAE,CAAA;IAC9B,IAAI,KAAK,IAAI,MAAM;QAAE,MAAM,CAAC,QAAQ,GAAG,SAAS,CAAC,OAAO,EAA8B,CAAA;IACtF,IAAI,MAAM;QAAE,MAAM,CAAC,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAA6B,CAAA;IACnH,IAAI,KAAK;QAAE,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,OAAO,EAA8B,CAAA;IACrE,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,IAAsB,EAAE,MAAmB;IACjE,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;IACrC,MAAM,EAAE,GAA6C,EAAE,CAAA;IACvD,MAAM,IAAI,GAA6C,EAAE,CAAA;IACzD,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YAC5C,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,QAAS,CAAC,CAAC,CAAE,GAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAY,CAAC,CAAA;YAClE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI;gBAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACrB,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QAC3M,MAAM,GAAG,GAAa,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;QACrF,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YACvB,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,QAAS,CAAC,CAAC,CAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC,CAAA;YAC/C,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACxC,CAAC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACrB,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,WAAW,CAAC,OAAgC,EAAE,IAA6B;IACzF,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,GAAG,KAAK,MAAM;YAAE,SAAQ;QAC5B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QACxB,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAA;QACxH,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;IAC9B,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AAC/C,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,QAAQ,CAAC,KAAkC,EAAE,KAAiC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;IACnH,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3B,IAAI,CAAC,KAAK;YAAE,SAAQ;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,CAAA;QACrD,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACnI,KAAK,MAAM,MAAM,IAAI,OAAO;YAAE,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1H,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IACnG,MAAM,KAAK,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC7I,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC;QAC3D,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC;QAC7D,oHAAoH;QACpH,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC;KAC7D,CAAA;AACH,CAAC","sourcesContent":["import * as THREE from 'three'\nimport { buildFigureGeometry } from '@hatiolab/things-scene'\n\nimport type { GizmoResult } from './gizmo.js'\nimport type { FigureRenderPart } from './renderer.js'\n\n/*\n * bodies <-> scene components, in one place.\n *\n * The modeller stands on a things-scene board in editing mode: each body figure-model evaluates is a\n * `figure-part` component, and what the scene's gizmo writes back comes here to become a change of the part's\n * inputs. The two vocabularies:\n *\n * (asset frame, mm, y up) scene component\n * ──────────────────────────────── ─────────────────────────────────────────────\n * body box extent x · y · z width · depth · height\n * body box centre x left + width/2 - board width/2\n * y zPos + depth/2\n * z top + height/2 - board height/2\n * body rotation (rigid@1, Rx·Ry·Rz) rotationX · -rotation · rotationY (radians; three 'XYZ')\n *\n * The box is the body's own bounding box in its local frame, so a turned body keeps its size and the gizmo's handles\n * sit on its faces. Its centre is not always the body's origin (a portal rises from its bottom), so a turn about the\n * centre moves the origin: the result gives the origin where the turned body puts it.\n */\n\nexport interface Board {\n width: number\n height: number\n}\n\nexport interface BodyFrame {\n /** The body's box: its centre in the body's own frame, and its extent. */\n centre: THREE.Vector3\n extent: THREE.Vector3\n rotation: THREE.Matrix4\n origin: THREE.Vector3\n}\n\nconst round = (n: number) => Math.round(n * 1e6) / 1e6\n\nexport function frameOf(part: FigureRenderPart): BodyFrame | null {\n /* The box things-scene's `figure-part` centres the body on: the same builder, measured the same way. */\n let built: THREE.BufferGeometry | undefined\n try {\n built = buildFigureGeometry(part.shape as never, part.segments) as THREE.BufferGeometry | undefined\n } catch {\n return null\n }\n if (!built?.getAttribute('position')) return null\n built.computeBoundingBox()\n const box = built.boundingBox!\n const unit = { centre: box.getCenter(new THREE.Vector3()), extent: box.getSize(new THREE.Vector3()) }\n built.dispose()\n const r = part.pose.r\n const rotation = new THREE.Matrix4().set(r[0]![0]!, r[0]![1]!, r[0]![2]!, 0, r[1]![0]!, r[1]![1]!, r[1]![2]!, 0, r[2]![0]!, r[2]![1]!, r[2]![2]!, 0, 0, 0, 0, 1)\n return { centre: unit.centre, extent: unit.extent, rotation, origin: new THREE.Vector3(...(part.pose.t as [number, number, number])) }\n}\n\n/** The colours a body is drawn in, from the palette the modeller resolved (the asset's, over which the theme wins). */\nfunction colours(part: FigureRenderPart, palette: Record<string, string>) {\n const m = part.material\n const glow = m.emissive?.on ? { color: palette[m.emissive.token] ?? palette[m.token] ?? '#9a9a9a', intensity: m.emissive.intensity } : undefined\n return { color: palette[m.token] ?? '#9a9a9a', preset: m.preset, transparent: !!m.transparent, ...(glow ? { glow } : {}) }\n}\n\n/** The component state that draws a body where figure-model put it. */\nexport function componentOf(part: FigureRenderPart, palette: Record<string, string>, board: Board): Record<string, unknown> | null {\n const frame = frameOf(part)\n if (!frame) return null\n const { centre, extent, rotation, origin } = frame\n const world = centre.clone().applyMatrix4(rotation).add(origin)\n const euler = new THREE.Euler().setFromRotationMatrix(rotation, 'XYZ')\n const width = extent.x || 1,\n depth = extent.y || 1,\n height = extent.z || 1\n return {\n type: 'figure-part',\n name: part.id,\n shape: part.shape,\n ...(part.segments !== undefined ? { segments: part.segments } : {}),\n ...colours(part, palette),\n width: round(width),\n depth: round(depth),\n height: round(height),\n left: round(world.x + board.width / 2 - width / 2),\n top: round(world.z + board.height / 2 - height / 2),\n zPos: round(world.y - depth / 2),\n rotationX: round(euler.x),\n rotation: round(-euler.y),\n rotationY: round(euler.z)\n }\n}\n\nconst num = (v: unknown, fallback = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback)\n\n/**\n * What the gizmo did, in the inputs' terms: where the body's origin is now, how it is turned (degrees, rigid@1's\n * order) and how much each of its own axes grew. Only what changed is given; a component the gizmo did not move gives\n * nothing.\n */\nexport function gizmoResultOf(part: FigureRenderPart, after: Record<string, unknown>, board: Board): GizmoResult {\n const frame = frameOf(part)\n if (!frame) return {}\n const { centre, extent, rotation, origin } = frame\n const width = num(after.width, extent.x),\n depth = num(after.depth, extent.y),\n height = num(after.height, extent.z)\n const scale = new THREE.Vector3(width / (extent.x || 1), depth / (extent.y || 1), height / (extent.z || 1))\n const world = new THREE.Vector3(num(after.left) + width / 2 - board.width / 2, num(after.zPos) + depth / 2, num(after.top) + height / 2 - board.height / 2)\n const euler = new THREE.Euler(num(after.rotationX), -num(after.rotation), num(after.rotationY), 'XYZ')\n const turnedTo = new THREE.Matrix4().makeRotationFromEuler(euler)\n\n const before = new THREE.Quaternion().setFromRotationMatrix(rotation)\n const now = new THREE.Quaternion().setFromEuler(euler)\n const turned = before.angleTo(now) > 1e-4\n const sized = [scale.x, scale.y, scale.z].some(s => Math.abs(s - 1) > 1e-4)\n /* The origin that puts the (scaled, turned) box centre where the scene has it. */\n const newOrigin = world.clone().sub(centre.clone().multiply(scale).applyMatrix4(turnedTo))\n const moved = newOrigin.distanceTo(origin) > 0.5\n\n const result: GizmoResult = {}\n if (moved || turned) result.position = newOrigin.toArray() as [number, number, number]\n if (turned) result.rotation = [euler.x, euler.y, euler.z].map(THREE.MathUtils.radToDeg) as [number, number, number]\n if (sized) result.scale = scale.toArray() as [number, number, number]\n return result\n}\n\n/**\n * A drag as amounts, for a part whose place is worked out (`nudge-part`): how far its origin moved and how far each of\n * its angles turned (rigid@1's order, degrees). Nothing on an axis that did not change.\n */\nexport function nudgeOf(part: FigureRenderPart, result: GizmoResult): { by: Partial<Record<'x' | 'y' | 'z', number>>; turn: Partial<Record<'x' | 'y' | 'z', number>> } {\n const axes = ['x', 'y', 'z'] as const\n const by: Partial<Record<'x' | 'y' | 'z', number>> = {}\n const turn: Partial<Record<'x' | 'y' | 'z', number>> = {}\n if (result.position) axes.forEach((axis, i) => {\n const d = round(result.position![i]! - (part.pose.t[i] as number))\n if (Math.abs(d) > 1e-6) by[axis] = d\n })\n if (result.rotation) {\n const r = part.pose.r\n const now = new THREE.Euler().setFromRotationMatrix(new THREE.Matrix4().set(r[0]![0]!, r[0]![1]!, r[0]![2]!, 0, r[1]![0]!, r[1]![1]!, r[1]![2]!, 0, r[2]![0]!, r[2]![1]!, r[2]![2]!, 0, 0, 0, 0, 1), 'XYZ')\n const was: number[] = [now.x, now.y, now.z].map(radians => (radians * 180) / Math.PI)\n axes.forEach((axis, i) => {\n const d = round(result.rotation![i]! - was[i]!)\n if (Math.abs(d) > 1e-6) turn[axis] = d\n })\n }\n return { by, turn }\n}\n\n/** The keys of `next` whose values differ from `current` -- only those are set, so the scene raises no echo. */\nexport function changedKeys(current: Record<string, unknown>, next: Record<string, unknown>): Record<string, unknown> | null {\n const diff: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(next)) {\n if (key === 'type') continue\n const was = current[key]\n const same = typeof value === 'object' && value !== null ? JSON.stringify(was) === JSON.stringify(value) : was === value\n if (!same) diff[key] = value\n }\n return Object.keys(diff).length ? diff : null\n}\n\n/** A board wide and deep enough for the figure, with room around it; its size only grows while a figure is edited. */\nexport function boardFor(parts: readonly FigureRenderPart[], at: Board & { depth?: number } = { width: 0, height: 0 }): Board & { depth: number } {\n const box = new THREE.Box3()\n for (const part of parts) {\n const frame = frameOf(part)\n if (!frame) continue\n const half = frame.extent.clone().multiplyScalar(0.5)\n const corners = [-1, 1].flatMap(x => [-1, 1].flatMap(y => [-1, 1].map(z => new THREE.Vector3(x * half.x, y * half.y, z * half.z))))\n for (const corner of corners) box.expandByPoint(corner.add(frame.centre).applyMatrix4(frame.rotation).add(frame.origin))\n }\n const size = box.isEmpty() ? new THREE.Vector3(1000, 1000, 1000) : box.getSize(new THREE.Vector3())\n const reach = (axis: 'x' | 'z') => 2 * Math.max(Math.abs(box.isEmpty() ? 500 : box.min[axis]), Math.abs(box.isEmpty() ? 500 : box.max[axis]))\n return {\n width: Math.max(at.width, Math.ceil(reach('x') * 1.2), 200),\n height: Math.max(at.height, Math.ceil(reach('z') * 1.2), 200),\n /* Only grows, as the plan does: a raised arm made the board taller than it was, and a new board lost the camera. */\n depth: Math.max(at.depth ?? 0, Math.ceil(size.y * 1.1), 200)\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import './canvas.js';
2
+ import type { FigureRenderPart } from './renderer.js';
3
+ export declare function captureFigureThumbnail(parts: FigureRenderPart[], palette: Record<string, string>): Promise<string | undefined>;
@@ -0,0 +1,75 @@
1
+ import './canvas.js';
2
+ /**
3
+ * 카드에 쓸 그림을 찍는다 — **숨은 자리에 캔버스를 하나 더 세워서.**
4
+ *
5
+ * ## 왜 보이는 캔버스를 찍지 않나
6
+ *
7
+ * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있다. 그것을 찍으면 카드마다 프레임이
8
+ * 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이 된다. 목록은 견주는 자리이므로 그림은
9
+ * **정해진 크기와 정해진 카메라**로 찍는다 — 새로 세운 캔버스(`viewOnly`)가 스스로 `fit()` 한 자리에서
10
+ * `snapshot()` 으로 찍는다. 캔버스가 씬 위로 옮겨 가면서 찍는 길이 빠졌고, 그동안 저장은 그림 없이 갔다
11
+ * (사용자 2026-09-24).
12
+ *
13
+ * ## 왜 서버가 아니라 브라우저인가
14
+ *
15
+ * 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주 실패한다. 도형을 그릴 GPU 는
16
+ * 이미 보는 사람 손에 있다.
17
+ *
18
+ * ## 못 찍어도 저장은 간다
19
+ *
20
+ * 탭이 뒤로 가면 브라우저가 `requestAnimationFrame` 을 멈춘다. 그 위에 저장이 얹혀 있으면 저장이
21
+ * 통째로 멈춘다 — 단추는 잠긴 채로, 아무 말도 없이. 그래서 시계와 경주시키고, 늦으면 그림 없이
22
+ * 간다. 정본이 그림보다 중하고, 그림은 다음에 저작면을 열 때 다시 채운다.
23
+ */
24
+ /** 카드는 4:3 이다. */
25
+ const WIDTH = 480;
26
+ const HEIGHT = 360;
27
+ /** 넘으면 그림 없이 저장한다. 빠르기가 아니라 저장이 멈추지 않는 것이 목적이다. */
28
+ const PATIENCE = 5000;
29
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
30
+ /** 씬이 서고 카메라가 자리를 잡을 틈. rAF 가 멈춘 탭에서도 풀리도록 시계와 경주시킨다. */
31
+ function settle() {
32
+ const frames = new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150))));
33
+ return Promise.race([frames, delay(600)]);
34
+ }
35
+ function within(work, ms) {
36
+ return Promise.race([work, delay(ms).then(() => undefined)]);
37
+ }
38
+ export async function captureFigureThumbnail(parts, palette) {
39
+ if (!parts.length)
40
+ return undefined;
41
+ /*
42
+ 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.
43
+ WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).
44
+ */
45
+ const host = document.createElement('div');
46
+ host.setAttribute('figure-thumbnail-stage', '');
47
+ host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`;
48
+ document.body.appendChild(host);
49
+ const shooter = document.createElement('figure-canvas');
50
+ shooter.parts = parts;
51
+ shooter.palette = palette;
52
+ shooter.context = 'thumbnail';
53
+ shooter.viewOnly = true;
54
+ shooter.style.cssText = 'display: block; width: 100%; height: 100%;';
55
+ try {
56
+ host.appendChild(shooter);
57
+ const shot = await within((async () => {
58
+ await settle();
59
+ shooter.fit?.();
60
+ await settle();
61
+ return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined;
62
+ })(), PATIENCE);
63
+ if (!shot)
64
+ console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`);
65
+ return shot;
66
+ }
67
+ catch (e) {
68
+ console.warn(`[thumbnail] 세우지 못했습니다 — ${e.message}`);
69
+ return undefined;
70
+ }
71
+ finally {
72
+ host.remove();
73
+ }
74
+ }
75
+ //# sourceMappingURL=thumbnail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnail.js","sourceRoot":"","sources":["../../client/modeller/thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAIpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AAElB,mDAAmD;AACnD,MAAM,QAAQ,GAAG,IAAI,CAAA;AAWrB,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAEnF,yDAAyD;AACzD,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;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,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,sBAAsB,CAC1C,KAAyB,EACzB,OAA+B;IAE/B,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAEnC;;;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,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,OAAO,GAAG,OAAO,CAAA;IACzB,OAAO,CAAC,OAAO,GAAG,WAAW,CAAA;IAC7B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QACzB,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,CAAC,GAAG,EAAE,EAAE,CAAA;YACf,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;QACD,IAAI,CAAC,IAAI;YAAE,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QAC7E,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 './canvas.js'\n\nimport type { FigureRenderPart } from './renderer.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있다. 그것을 찍으면 카드마다 프레임이\n * 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이 된다. 목록은 견주는 자리이므로 그림은\n * **정해진 크기와 정해진 카메라**로 찍는다 — 새로 세운 캔버스(`viewOnly`)가 스스로 `fit()` 한 자리에서\n * `snapshot()` 으로 찍는다. 캔버스가 씬 위로 옮겨 가면서 찍는 길이 빠졌고, 그동안 저장은 그림 없이 갔다\n * (사용자 2026-09-24).\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주 실패한다. 도형을 그릴 GPU 는\n * 이미 보는 사람 손에 있다.\n *\n * ## 못 찍어도 저장은 간다\n *\n * 탭이 뒤로 가면 브라우저가 `requestAnimationFrame` 을 멈춘다. 그 위에 저장이 얹혀 있으면 저장이\n * 통째로 멈춘다 — 단추는 잠긴 채로, 아무 말도 없이. 그래서 시계와 경주시키고, 늦으면 그림 없이\n * 간다. 정본이 그림보다 중하고, 그림은 다음에 저작면을 열 때 다시 채운다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\n/** 넘으면 그림 없이 저장한다. 빠르기가 아니라 저장이 멈추지 않는 것이 목적이다. */\nconst PATIENCE = 5000\n\ninterface Shooter extends HTMLElement {\n parts?: FigureRenderPart[]\n palette?: Record<string, string>\n context?: string\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n fit?: () => void\n}\n\nconst delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))\n\n/** 씬이 서고 카메라가 자리를 잡을 틈. rAF 가 멈춘 탭에서도 풀리도록 시계와 경주시킨다. */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n return Promise.race([frames, delay(600)])\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 captureFigureThumbnail(\n parts: FigureRenderPart[],\n palette: Record<string, string>\n): Promise<string | undefined> {\n if (!parts.length) return undefined\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.parts = parts\n shooter.palette = palette\n shooter.context = 'thumbnail'\n shooter.viewOnly = true\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n const shot = await within(\n (async () => {\n await settle()\n shooter.fit?.()\n await settle()\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n })(),\n PATIENCE\n )\n if (!shot) console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\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"]}
@@ -68,7 +68,7 @@ export declare class FigureListPage extends FigureListPageBase {
68
68
  total: number;
69
69
  records: Figure[];
70
70
  }>;
71
- /** The modeller that can edit this figure, by its format; a new figure starts in V3 (`modeller-route.ts`). */
71
+ /** The modeller that can edit this figure, by its format; a new figure starts in (`modeller-route.ts`). */
72
72
  private openModeller;
73
73
  /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */
74
74
  private removeFigure;
@@ -404,11 +404,11 @@ let FigureListPage = class FigureListPage extends FigureListPageBase {
404
404
  말이 알려 준다.
405
405
 
406
406
  색칠한 배지로 두지 않는다. 옛 draw call 한도 3 으로 재던 때는 카탈로그 거의 모두가 0 점이라
407
- 붉은 덩어리가 카드마다 섰고 아무것도 가르지 못했다. V3 는 「한 화면 N 대」 목표에 견준
408
- 여유라(`v3ScoreOf`, 2026-09-24) 퍼지지만, 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.
407
+ 붉은 덩어리가 카드마다 섰고 아무것도 가르지 못했다. 이 모델은 「한 화면 N 대」 목표에 견준
408
+ 여유라(`figureScoreOf`, 2026-09-24) 퍼지지만, 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.
409
409
 
410
410
  아직 재지 않은 것을 0 으로 보여 주지 않는다 — 0 은 「가볍다」는 뜻이 되어 버린다.
411
- 등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` · `v3ScoreOf` 가 정하고,
411
+ 등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` · `figureScoreOf` 가 정하고,
412
412
  화면이 그것을 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 말한다.
413
413
  */
414
414
  renderer: (value) => value == null
@@ -550,7 +550,7 @@ let FigureListPage = class FigureListPage extends FigureListPageBase {
550
550
  const { items, total } = await fetchFigureList({ page, limit, filters, sortings });
551
551
  return { total: total || 0, records: items || [] };
552
552
  }
553
- /** The modeller that can edit this figure, by its format; a new figure starts in V3 (`modeller-route.ts`). */
553
+ /** The modeller that can edit this figure, by its format; a new figure starts in (`modeller-route.ts`). */
554
554
  openModeller(figure) {
555
555
  navigate(modellerPathOf(figure));
556
556
  }
@@ -1 +1 @@
1
- {"version":3,"file":"figure-list-page.js","sourceRoot":"","sources":["../../client/pages/figure-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist'\nimport '@material/web/icon/icon.js'\nimport '../viewparts/figure-thumb.js'\n\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { i18next, localize } from '@operato/i18n'\nimport { p13n } from '@operato/p13n'\nimport { auth } from '@things-factory/auth-base/dist-client'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\n\nimport { capacityFrom } from '../modeller/scene-budget.js'\nimport { deleteFigure, fetchFigureList } from '../graphql/index.js'\nimport type { Figure } from '../types.js'\nimport { modellerPathOf } from '../modeller/modeller-route.js'\n\n/**\n * 이 값 아래면 점수를 색으로 눈에 띄게 한다.\n *\n * **등급 경계가 아니다.** 등급(A~D)은 `figure-model` 의 `scoreOf` 가 정하고 화면은 그것을\n * 베끼지 않는다. 이것은 「눈에 걸리게 할 선」이고, 화면의 판단이다.\n */\nconst ATTENTION_BELOW = 50\n\n/*\n * ── renderer 가 그리는 것은 이 파일의 CSS 가 닿지 않는다 ──────────────────────\n *\n * 컬럼 renderer 의 결과는 `ox-card-field` 의 shadow root 안에 그려진다. 이 페이지의 `static\n * styles` 는 그 경계를 넘지 못하므로 `code[figure-type]` 같은 선택자는 **아무 일도 하지 않는다.**\n * 넘어가는 것은 custom property 뿐이라, 색과 글꼴은 여기서 인라인으로 준다 — 값 자체는 테마\n * 토큰을 가리키므로 밝음·어둠은 따라온다. data-grist 의 image renderer 도 같은 방식이다.\n */\nconst CAPACITY = 'font-weight: 700; color: var(--md-sys-color-primary);'\nconst UNMEASURED = 'font-style: italic; color: var(--md-sys-color-outline);'\nconst RELEASED = 'font-weight: 700; color: var(--md-sys-color-primary);'\nconst DRAFT = 'color: var(--md-sys-color-tertiary);'\n/**\n * 조용히 적는 숫자. 낮은 것만 점으로 눈에 걸리게 한다.\n *\n * **`inline-flex` 를 쓰지 않는다.** 썼더니 점이 뜨는 카드에서만 이 값이 옆의 두 값과 밑줄이\n * 어긋났다 — inline-flex 상자의 밑줄은 **첫 항목**에서 오는데, 첫 항목이 글자 없는 점이라\n * 밑줄이 없어 브라우저가 상자 아래를 밑줄로 삼는다. 점이 없는 카드(50 이상)는 멀쩡했으므로\n * 한 줄에 세 값이 나란히 설 때만 눈에 띄었다.\n *\n * 그냥 인라인으로 두면 이 상자의 밑줄이 곧 숫자의 밑줄이라 옆의 둘과 맞는다. 점은 제 몫으로\n * 글자 가운데에 맞춘다.\n */\nconst SCORE = 'font-variant-numeric: tabular-nums;'\nconst LOW_DOT =\n 'display: inline-block; width: 5px; height: 5px; margin-right: 4px; vertical-align: middle; ' +\n 'border-radius: 50%; background-color: var(--md-sys-color-error);'\n\n/*\n * `as typeof PageView` 로 조이지 않는다 — 그렇게 하면 p13n 이 얹어 주는\n * `getPagePreferenceProvider` 가 타입에서 사라져 개인 설정을 붙일 수 없다.\n */\nconst FigureListPageBase = p13n(localize(i18next)(PageView))\n\n/**\n * Figure 카탈로그.\n *\n * 이 화면은 저작면으로 가는 문이다. 여기서 오래 머물 화면이 아니므로 꾸미지 않는다. 카드에는\n * **고를 때 쓰는 것만** 싣는다 — 그림과 이름, 그리고 아직 초안이라는 것. 배치 한도·성숙도·삼각형\n * 수는 고른 뒤에 따지는 값이라 표 모드의 칸으로 남기고 저작면에서 본다.\n *\n * ## 왜 손으로 만든 카드가 아니라 격자인가\n *\n * 전에는 카드 격자와 걸러 보기 줄과 빈 화면을 이 파일이 직접 그렸다(200줄). 그 자리에서 빠져\n * 있던 것들은 하나같이 `ox-grist` 가 이미 가진 것이다 — 쪽 나누기, 정렬, 컬럼 걸러 보기,\n * 개인 설정(모드·정렬·쪽 크기를 사람마다 기억한다), 표 모드로 바꿔 보기, 내보내기.\n * 손으로 만든 카드는 그 중 아무것도 없었고, 목록이 늘어나면 전부 한 번에 그렸다.\n *\n * 카드에 실을 것은 `list` 가 정하고, 무엇을 어떻게 그릴지는 컬럼의 renderer 가 정한다. 같은\n * 설정이 표 모드에서도 그대로 쓰이므로 **두 모드가 다른 말을 하지 않는다.**\n */\n@customElement('figure-list-page')\nexport class FigureListPage extends FigureListPageBase {\n static styles = [\n /* The frame owns the page shell; the grist is the content it sizes (ADR-0060 decision 8). */\n WorkSurfacePageStyles,\n ScrollbarStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n css`\n :host {\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n /* 떠 있는 단추의 기준 상자 */\n position: relative;\n }\n\n /*\n ── 만들기 단추 ────────────────────────────────────────────────────────\n 이 화면의 주된 동작 하나. 목록 위에 떠 있고 자리를 먹지 않는다.\n\n 글자를 함께 둔다(extended FAB). 아이콘만 두면 처음 온 사람은 + 표가 무엇을 만드는지\n 모른다 — 이 화면에서 만들 수 있는 것이 무엇인지가 그 글자다.\n */\n button[fab] {\n position: absolute;\n right: var(--spacing-large, 12px);\n bottom: var(--spacing-large, 12px);\n z-index: 10;\n\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 10px 18px 10px 14px;\n font: var(--label-font, inherit);\n font-size: 0.82rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n /* 떠 있는 것의 모서리는 알약이다 — 담는 면(6px)을 따르지 않는다 */\n border-radius: var(--md-sys-shape-corner-full, 999px);\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);\n cursor: pointer;\n }\n button[fab]:hover {\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n }\n button[fab] md-icon {\n --md-icon-size: 20px;\n }\n\n /* 격자가 바깥에 요구하는 것은 확정된 높이 하나뿐이다 — 스크롤은 격자가 안에서 한다 */\n /*\n ── 그림이 주인공인 카탈로그 ────────────────────────────────────────\n 카드 기본값은 글이 많은 목록(직원·결재함)에 맞춰져 있다. 여기서는 사람이 먼저 보는 것이\n 형상 그림이므로, 그림 칸을 비율로 잡아 카드 폭이 달라져도 그림이 나란히 읽히게 한다.\n\n 그림은 카드 끝까지 닿는다 — 위쪽 모서리만 카드와 같은 6px 로 깎아 한 장처럼 보이게 하고,\n 그림 안에 얹히는 초안 표시·삭제는 figure-thumb 이 그린다.\n */\n ox-grist {\n --data-card-min-width: 240px;\n --data-card-record-card-border-radius: 6px;\n --data-card-record-card-padding: 0;\n --data-card-thumbnail-height: auto;\n --data-card-thumbnail-aspect: 4 / 3;\n --data-card-thumbnail-padding: 0;\n --data-card-thumbnail-radius: 6px 6px 0 0;\n --data-card-thumbnail-background: var(--md-sys-color-surface-container);\n\n /*\n 카드 기본 글꼴은 굵다(제목 bold 1.1em). 글이 정보를 나르는 목록에서는 맞지만, 여기서\n 정보를 나르는 것은 그림이다. 굵은 제목이 카드마다 서면 그림보다 글이 먼저 읽힌다 —\n 이름은 알아볼 정도로만, 타입은 곁글로 둔다.\n */\n --data-card-item-name-font: 500 0.9rem/1.35 var(--theme-font, 'Roboto');\n --data-card-item-name-color: var(--md-sys-color-on-surface);\n --data-card-item-disc-font: normal 0.72rem/1.2 var(--theme-font, 'Roboto');\n --data-card-item-disc-color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n --data-card-item-etc-font: normal 0.72rem/1.2 var(--theme-font, 'Roboto');\n\n /* 상태와 성숙도는 짧은 표식이다 — 두 줄로 세우지 않고 한 줄에 뉜다 */\n --data-card-detail-direction: row;\n\n /*\n 맨 위로 가는 화살표를 만들기 단추 **위로** 올린다. 둘 다 오른쪽 아래에 떠 있으므로\n 그냥 두면 겹친다 — 단추(36px + 아래 여백 12px) 위에 12px 을 띄운 자리다.\n */\n --data-card-fab-position-vertical: 64px;\n --data-list-fab-position-vertical: 64px;\n }\n `\n ]\n\n @property({ type: Object }) gristConfig: any\n\n @query('ox-grist') private grist!: DataGrist\n\n private listen?: () => void\n\n get context() {\n return {\n title: i18next.t('figure.title.figures'),\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || '',\n placeholder: i18next.t('figure.text.search-figure')\n },\n filter: {\n handler: () => {\n this.grist.toggleHeadroom()\n }\n }\n /*\n **만들기는 `actions` 가 아니라 FAB 이다.**\n\n `context.actions` 로 두면 페이지 바닥 줄에 글자 단추로 서는데, 그 줄은 「지금 화면에 대해\n 할 일」을 늘어놓는 자리다. 만들기는 이 화면의 유일한 주된 동작이므로 줄에 섞이지 않고\n 떠 있어야 한다. 카드를 가리지 않는 오른쪽 아래, 손이 가는 자리다(아래 render 의 [fab]).\n */\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n mode=\"CARD\"\n @search-tag=${(e: CustomEvent) => this.findByTag(e.detail.tag)}\n .config=${this.gristConfig}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n <ox-filters-form autofocus without-search></ox-filters-form>\n </div>\n </div>\n\n <ox-grist-personalizer slot=\"setting\"></ox-grist-personalizer>\n </ox-grist>\n\n <button fab title=${i18next.t('figure.button.new-figure')} @click=${() => this.openModeller()}>\n <md-icon>add</md-icon>${i18next.t('figure.button.new-figure')}\n </button>\n `\n }\n\n /**\n * 카드의 태그 알약을 눌렀다 — **그 태그로 목록을 거른다.**\n *\n * 검색창에 그 말을 채워 넣는다. 격자에 조건만 걸고 창을 비워 두면 저작자는 **왜 목록이\n * 줄었는지 모른 채** 보게 되고, 되돌리려면 무엇을 지워야 하는지도 알 수 없다.\n *\n * 서버가 `tags` 뿐 아니라 이름·타입·설명·분류를 함께 훑으므로, 그 말이 다른 칸에 든 도형도\n * 같이 온다. 태그만 정확히 거르는 것이 아니라 **찾기**다.\n */\n private findByTag(tag: string) {\n this.grist.searchText = tag\n /* 창의 값은 `context` 에서 나온다 — 다시 알려야 헤더가 새 값을 그린다. */\n this.updateContext()\n }\n\n async pageInitialized() {\n this.gristConfig = {\n /*\n **쪽 나누기는 선택이 아니다.** 격자에 가상 스크롤이 없어 받은 것을 전부 DOM 에 그린다.\n 카드 모드는 끝까지 내려가면 다음 쪽을 이어 붙이므로, 한 쪽이 작아도 목록은 계속 읽힌다.\n */\n pagination: { pages: [24, 48, 96] },\n\n /*\n 아직 아무것도 없을 때 하는 말. **이 자리가 이 화면에서 가장 중요하다** — 처음 온 사람은\n 여기서 이 도구가 무엇을 만드는 것인지 알게 된다. 조건이 걸려 비었을 때는 격자가 스스로\n 다른 말을 한다(「찾는 것이 없다」). 그 둘을 섞지 않는 것이 격자 쪽에 이미 들어 있다.\n */\n emptyNote: {\n icon: 'deployed_code',\n title: i18next.t('figure.text.no-figure-yet'),\n description: i18next.t('figure.text.what-a-figure-is')\n },\n\n list: {\n /* 그림 칸은 그 컬럼의 renderer 가 그린다 — 그림 없을 때의 안내까지 `figure-thumb` 안이다 */\n thumbnail: 'thumbnail',\n fields: ['name', 'type'],\n /*\n ── 무게를 모른 채 고르게 두지 않는다 ────────────────────────────────\n 이름과 그림만 실으면 저작자는 **이 형상이 무거운지 모른 채** 고른다. 그것을 저작면에\n 들어가서야 아는 것은 늦다. 그래서 격자로 옮기기 전 손 카드가 실었던 셋을 그대로 싣는다 —\n 몇 개까지 놓을 수 있나(배치 한도) · 성숙도 · 삼각형 수.\n\n 한 줄에 뉜다(`--data-card-detail-direction: row`). 이름표는 달지 않는다 — 값 자체가\n 「625개까지」·「삼각형 440개」로 읽히고, 성숙도만 숫자 하나여서 손이 갔을 때 이름이 뜬다.\n\n 상태는 이 줄에 없다. 그림 위 초안 표시가 그 일을 한다(`figure-thumb`) — 발행된 것에는\n 아무 말도 하지 않으므로 빈 칸이 생기지 않는다.\n */\n details: ['capacity', 'score', 'triangles']\n },\n\n columns: [\n { type: 'gutter', gutterName: 'sequence', fixed: true },\n {\n /*\n 표 모드에서만 쓰는 삭제 칸. 카드에서는 `figure-thumb` 이 그림 위에 그리므로\n `forList: false` 로 내린다 — 카드 아래 단추 줄은 감춰도 자리가 남는다.\n */\n type: 'gutter',\n gutterName: 'button',\n icon: 'delete',\n title: i18next.t('figure.button.delete-figure'),\n danger: true,\n forList: false,\n width: 40,\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: Figure) => this.removeFigure(record)\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('figure.label.name'),\n record: { editable: false },\n filter: 'search',\n sortable: true,\n width: 200\n },\n {\n /*\n 타입 이름은 **저장되는 식별자**다. 표시 이름과 다르게 보이게 두어야 도면이 무엇으로\n 이 형상을 찾는지 헷갈리지 않는다.\n */\n type: 'string',\n name: 'type',\n header: i18next.t('figure.label.figure-type'),\n record: {\n editable: false,\n renderer: (value: string) => html`<code>${value}</code>`\n },\n filter: 'search',\n sortable: true,\n width: 160\n },\n {\n /*\n 태그 — **카드에서 곁줄로 보이고 검색에 걸린다.**\n\n 서버가 이미 `type`·`name`·`description`·`category`·`tags` 를 함께 훑는다. 그런데\n 화면에는 태그가 어디에도 안 보여서, 저작자가 적은 태그가 **자기 눈에 돌아오지 않았다** —\n 적어 둔 것이 쓰이는지 알 수 없으면 아무도 적지 않는다.\n\n 정렬은 걸지 않는다. 배열이라 무엇을 기준으로 줄을 세우는지 답할 수 없다.\n\n **카드에서는 이 컬럼이 그리지 않는다.** 본문에 한 줄로 두었더니 태그가 있는 카드만\n 높아져 격자가 어긋났다 — 개수가 도형마다 다르므로 본문에 두는 한 남는 문제다.\n 카드에서는 `figure-thumb` 이 그림 위에 얹는다. 여기 남은 것은 표 모드의 칸이다.\n */\n type: 'string',\n name: 'tags',\n header: i18next.t('figure.label.tags'),\n record: {\n editable: false,\n renderer: (value: string[] | null) => (value || []).join(' · ')\n },\n filter: 'search',\n sortable: false,\n width: 180\n },\n {\n type: 'string',\n name: 'state',\n header: i18next.t('figure.label.state'),\n record: {\n editable: false,\n /*\n **발행된 것에는 아무 말도 하지 않는다.** 카탈로그에 있는 것은 대개 발행된 것이라,\n 카드마다 「발행됨」이 붙으면 그 말이 아무것도 가르지 않고 자리만 먹는다. 판 번호도\n 고를 때 쓰는 값이 아니다 — 이력 탭에 있다.\n\n 표 모드에서는 칸에 값이 있어야 하므로 그때는 발행됨도 적는다(`wide` 가 아니라 label\n 이 붙는 자리인지로 가른다 — 카드는 label 을 달지 않았다).\n */\n renderer: (value: string, _column: any, record: Figure, _rowIndex: number, field: Element) => {\n if (value !== 'released') {\n return html`<span style=${DRAFT}>${i18next.t('figure.label.draft')}</span>`\n }\n\n /* 표에서는 칸이 비면 안 된다. 카드에서만 입을 다문다 — 그리는 자리가 알려 준다. */\n return field?.tagName === 'OX-CARD-FIELD'\n ? ''\n : html`<span style=${RELEASED}\n >${i18next.t('figure.label.figure-released')}\n ${record.version ? i18next.t('figure.text.version-number', { version: record.version }) : ''}</span\n >`\n }\n },\n /*\n 전체·초안·발행됨을 단추로 고른다 — 값이 둘뿐인 조건을 펼침 목록에 넣지 않는다.\n\n 이름표는 비워 둔다. 단추에 「초안」·「발행됨」이라고 적혀 있으니 그 앞의 「상태」는\n 같은 말을 한 번 더 하는 것이고, 걸러 보기 줄에서 자리만 먹는다.\n */\n filter: {\n type: 'select-buttons',\n operator: 'in',\n label: '',\n options: [\n { display: i18next.t('figure.label.draft'), value: 'draft' },\n { display: i18next.t('figure.label.figure-released'), value: 'released' }\n ]\n },\n width: 120\n },\n {\n /*\n **몇 개까지 놓을 수 있나.** 카드에는 싣지 않고 표 모드의 칸으로 둔다 — 고른 뒤에\n 따지는 값이다. 서버에 없는 값이라 `groups` 와 `triangles` 에서 계산하는데, 저작면\n 머리와 **같은 함수**를 쓴다. 두 화면이 같은 형상을 다른 잣대로 말하면 무엇을 고칠지\n 모른다.\n */\n type: 'string',\n name: 'capacity',\n header: i18next.t('figure.label.placement-limit'),\n sortable: false,\n record: {\n editable: false,\n renderer: (_value: unknown, _column: any, record: Figure) => {\n const capacity = capacityFrom(record.groups ?? 0, record.triangles ?? 0)\n\n return capacity\n ? html`<span style=${CAPACITY} title=${i18next.t('figure.text.reuse-score-explained')}>\n ${i18next.t('figure.text.fits-n-instances', { n: capacity.instances.toLocaleString() })}\n </span>`\n : html`<span style=${UNMEASURED}>${i18next.t('figure.text.not-measured-yet')}</span>`\n }\n },\n width: 120\n },\n {\n type: 'float',\n name: 'score',\n header: i18next.t('figure.label.maturity'),\n record: {\n editable: false,\n /*\n 카드에 남긴 하나뿐인 숫자. **이름표를 달지 않으므로** 무엇인지는 손이 갔을 때 뜨는\n 말이 알려 준다.\n\n 색칠한 배지로 두지 않는다. 옛 draw call 한도 3 으로 재던 때는 카탈로그 거의 모두가 0 점이라\n 붉은 덩어리가 카드마다 섰고 아무것도 가르지 못했다. V3 는 「한 화면 N 대」 목표에 견준\n 여유라(`v3ScoreOf`, 2026-09-24) 퍼지지만, 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.\n\n 아직 재지 않은 것을 0 으로 보여 주지 않는다 — 0 은 「가볍다」는 뜻이 되어 버린다.\n 등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` · `v3ScoreOf` 가 정하고,\n 화면이 그것을 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 말한다.\n */\n renderer: (value: number | null) =>\n value == null\n ? html`<span style=${UNMEASURED}>${i18next.t('figure.text.not-measured-yet')}</span>`\n : html`<span style=${SCORE} title=${i18next.t('figure.label.maturity')}>\n ${value < ATTENTION_BELOW ? html`<i style=${LOW_DOT}></i>` : ''}${Math.round(value)}\n </span>`\n },\n sortable: true,\n width: 90\n },\n {\n type: 'integer',\n name: 'triangles',\n header: i18next.t('figure.label.triangles'),\n record: {\n editable: false,\n /*\n 카드에서는 이름표 없이 서므로 숫자만 두면 옆의 성숙도와 구별되지 않는다. 무엇의\n 수인지 값이 스스로 말하게 한다 — 표 모드에서는 머리글이 그 일을 하므로 겹치지만,\n 그 겹침이 카드에서 뜻을 잃는 것보다 낫다.\n */\n renderer: (value: number | null) =>\n value == null ? '' : i18next.t('figure.text.triangle-count', { count: value })\n },\n sortable: true,\n width: 110\n },\n {\n type: 'integer',\n name: 'groups',\n header: i18next.t('figure.label.budget-groups'),\n record: { editable: false },\n hidden: true,\n width: 0\n },\n {\n /*\n **카드의 그림 칸 전체를 이 renderer 가 그린다.**\n\n 기본 image renderer 는 그림만 그리고, 그 위에 얹혀야 하는 것(초안 표시·삭제)은 카드\n 아래 gutter 줄로 밀린다. 격자로 옮기기 전 손 카드가 그것을 그림 위에 두었고 그 편이\n 맞다 — 그래서 그림 칸을 `figure-thumb` 에게 넘긴다. hover 가 필요해 컴포넌트여야\n 하는 사연은 그 파일에 적혀 있다.\n\n `renderEmpty` — 그림이 아직 없는 것도 자리를 지킨다. 그 카드에도 초안 표시와 삭제가\n 있어야 하고, 「그림 없음」이라고 말하는 것도 `figure-thumb` 이 한다.\n */\n type: 'image',\n name: 'thumbnail',\n header: i18next.t('field.thumbnail'),\n record: {\n editable: false,\n align: 'center',\n wide: true,\n renderEmpty: true,\n renderer: (_value: unknown, _column: any, record: Figure) => html`\n <figure-thumb .figure=${record} @remove=${() => this.removeFigure(record)}></figure-thumb>\n `\n },\n hidden: true,\n width: 160\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated_at'),\n label: true,\n record: { editable: false },\n sortable: true,\n width: 180\n },\n {\n /*\n `resource-object` 를 쓰지 않는다 — 그 renderer 는 다른 패키지(resource-ui)가 등록하고,\n 등록되지 않은 앱에서는 조용히 글자 renderer 로 떨어져 `[object Object]` 가 된다.\n 이름 하나 꺼내는 일에 그 위험을 질 이유가 없다.\n */\n type: 'string',\n name: 'updater',\n header: i18next.t('field.updater'),\n record: {\n editable: false,\n renderer: (value: { name?: string } | null) => value?.name || ''\n },\n hidden: true,\n width: 120\n }\n ],\n\n rows: {\n selectable: false,\n appendable: false,\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: Figure) => {\n if (record?.id) {\n this.openModeller(record)\n }\n }\n }\n },\n\n sorters: [{ name: 'updatedAt', desc: true }]\n }\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n /*\n **로그인 정보가 도착하면 다시 불러온다.**\n\n 화면이 서는 것과 프로필이 오는 것은 순서가 정해져 있지 않고, 목록 질의는 로그인된 사람으로\n 나간다. 첫 질의가 프로필보다 앞서면 그 한 번이 실패하고, 그 뒤로 다시 묻는 사람이 없으면\n 화면은 비어 있는 채로 남는다. lite-menu 도 같은 신호로 같은 순서 문제를 푼다\n (`auth.on('profile')` — 그쪽 주석에 그 사연이 적혀 있다).\n */\n this.listen = () => {\n if (this.active) {\n this.grist?.fetch()\n }\n }\n auth.on('profile', this.listen)\n }\n\n disconnectedCallback() {\n if (this.listen) {\n auth.off?.('profile', this.listen)\n this.listen = undefined\n }\n super.disconnectedCallback()\n }\n\n async pageUpdated() {\n if (this.active) {\n this.grist.fetch()\n }\n }\n\n /**\n * 걸러 보기·정렬·쪽을 **격자가 만든 그대로** 서버에 넘긴다.\n *\n * 여기서 이름을 바꿔 담으면 격자가 아는 조건과 서버에 가는 조건이 어긋난다. 실패는 격자가\n * 받아 화면에 낸다 — 목록이 비어 있는 것과 못 불러온 것은 다른 사실이다.\n */\n async fetchHandler({ page = 1, limit = 24, sortings = [], filters = [] }: FetchOption) {\n const { items, total } = await fetchFigureList({ page, limit, filters, sortings })\n\n return { total: total || 0, records: items || [] }\n }\n\n /** The modeller that can edit this figure, by its format; a new figure starts in V3 (`modeller-route.ts`). */\n private openModeller(figure?: Figure) {\n navigate(modellerPathOf(figure))\n }\n\n /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */\n private async removeFigure(figure: Figure) {\n // 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다\n const message = i18next.t('figure.text.delete-figure-confirm', { name: figure.name })\n if (!confirm(message)) {\n return\n }\n\n await deleteFigure(figure.id)\n this.grist.fetch()\n }\n}\n"]}
1
+ {"version":3,"file":"figure-list-page.js","sourceRoot":"","sources":["../../client/pages/figure-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist'\nimport '@material/web/icon/icon.js'\nimport '../viewparts/figure-thumb.js'\n\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { i18next, localize } from '@operato/i18n'\nimport { p13n } from '@operato/p13n'\nimport { auth } from '@things-factory/auth-base/dist-client'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\n\nimport { capacityFrom } from '../modeller/scene-budget.js'\nimport { deleteFigure, fetchFigureList } from '../graphql/index.js'\nimport type { Figure } from '../types.js'\nimport { modellerPathOf } from '../modeller/modeller-route.js'\n\n/**\n * 이 값 아래면 점수를 색으로 눈에 띄게 한다.\n *\n * **등급 경계가 아니다.** 등급(A~D)은 `figure-model` 의 `scoreOf` 가 정하고 화면은 그것을\n * 베끼지 않는다. 이것은 「눈에 걸리게 할 선」이고, 화면의 판단이다.\n */\nconst ATTENTION_BELOW = 50\n\n/*\n * ── renderer 가 그리는 것은 이 파일의 CSS 가 닿지 않는다 ──────────────────────\n *\n * 컬럼 renderer 의 결과는 `ox-card-field` 의 shadow root 안에 그려진다. 이 페이지의 `static\n * styles` 는 그 경계를 넘지 못하므로 `code[figure-type]` 같은 선택자는 **아무 일도 하지 않는다.**\n * 넘어가는 것은 custom property 뿐이라, 색과 글꼴은 여기서 인라인으로 준다 — 값 자체는 테마\n * 토큰을 가리키므로 밝음·어둠은 따라온다. data-grist 의 image renderer 도 같은 방식이다.\n */\nconst CAPACITY = 'font-weight: 700; color: var(--md-sys-color-primary);'\nconst UNMEASURED = 'font-style: italic; color: var(--md-sys-color-outline);'\nconst RELEASED = 'font-weight: 700; color: var(--md-sys-color-primary);'\nconst DRAFT = 'color: var(--md-sys-color-tertiary);'\n/**\n * 조용히 적는 숫자. 낮은 것만 점으로 눈에 걸리게 한다.\n *\n * **`inline-flex` 를 쓰지 않는다.** 썼더니 점이 뜨는 카드에서만 이 값이 옆의 두 값과 밑줄이\n * 어긋났다 — inline-flex 상자의 밑줄은 **첫 항목**에서 오는데, 첫 항목이 글자 없는 점이라\n * 밑줄이 없어 브라우저가 상자 아래를 밑줄로 삼는다. 점이 없는 카드(50 이상)는 멀쩡했으므로\n * 한 줄에 세 값이 나란히 설 때만 눈에 띄었다.\n *\n * 그냥 인라인으로 두면 이 상자의 밑줄이 곧 숫자의 밑줄이라 옆의 둘과 맞는다. 점은 제 몫으로\n * 글자 가운데에 맞춘다.\n */\nconst SCORE = 'font-variant-numeric: tabular-nums;'\nconst LOW_DOT =\n 'display: inline-block; width: 5px; height: 5px; margin-right: 4px; vertical-align: middle; ' +\n 'border-radius: 50%; background-color: var(--md-sys-color-error);'\n\n/*\n * `as typeof PageView` 로 조이지 않는다 — 그렇게 하면 p13n 이 얹어 주는\n * `getPagePreferenceProvider` 가 타입에서 사라져 개인 설정을 붙일 수 없다.\n */\nconst FigureListPageBase = p13n(localize(i18next)(PageView))\n\n/**\n * Figure 카탈로그.\n *\n * 이 화면은 저작면으로 가는 문이다. 여기서 오래 머물 화면이 아니므로 꾸미지 않는다. 카드에는\n * **고를 때 쓰는 것만** 싣는다 — 그림과 이름, 그리고 아직 초안이라는 것. 배치 한도·성숙도·삼각형\n * 수는 고른 뒤에 따지는 값이라 표 모드의 칸으로 남기고 저작면에서 본다.\n *\n * ## 왜 손으로 만든 카드가 아니라 격자인가\n *\n * 전에는 카드 격자와 걸러 보기 줄과 빈 화면을 이 파일이 직접 그렸다(200줄). 그 자리에서 빠져\n * 있던 것들은 하나같이 `ox-grist` 가 이미 가진 것이다 — 쪽 나누기, 정렬, 컬럼 걸러 보기,\n * 개인 설정(모드·정렬·쪽 크기를 사람마다 기억한다), 표 모드로 바꿔 보기, 내보내기.\n * 손으로 만든 카드는 그 중 아무것도 없었고, 목록이 늘어나면 전부 한 번에 그렸다.\n *\n * 카드에 실을 것은 `list` 가 정하고, 무엇을 어떻게 그릴지는 컬럼의 renderer 가 정한다. 같은\n * 설정이 표 모드에서도 그대로 쓰이므로 **두 모드가 다른 말을 하지 않는다.**\n */\n@customElement('figure-list-page')\nexport class FigureListPage extends FigureListPageBase {\n static styles = [\n /* The frame owns the page shell; the grist is the content it sizes (ADR-0060 decision 8). */\n WorkSurfacePageStyles,\n ScrollbarStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n css`\n :host {\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n /* 떠 있는 단추의 기준 상자 */\n position: relative;\n }\n\n /*\n ── 만들기 단추 ────────────────────────────────────────────────────────\n 이 화면의 주된 동작 하나. 목록 위에 떠 있고 자리를 먹지 않는다.\n\n 글자를 함께 둔다(extended FAB). 아이콘만 두면 처음 온 사람은 + 표가 무엇을 만드는지\n 모른다 — 이 화면에서 만들 수 있는 것이 무엇인지가 그 글자다.\n */\n button[fab] {\n position: absolute;\n right: var(--spacing-large, 12px);\n bottom: var(--spacing-large, 12px);\n z-index: 10;\n\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 10px 18px 10px 14px;\n font: var(--label-font, inherit);\n font-size: 0.82rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n /* 떠 있는 것의 모서리는 알약이다 — 담는 면(6px)을 따르지 않는다 */\n border-radius: var(--md-sys-shape-corner-full, 999px);\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);\n cursor: pointer;\n }\n button[fab]:hover {\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n }\n button[fab] md-icon {\n --md-icon-size: 20px;\n }\n\n /* 격자가 바깥에 요구하는 것은 확정된 높이 하나뿐이다 — 스크롤은 격자가 안에서 한다 */\n /*\n ── 그림이 주인공인 카탈로그 ────────────────────────────────────────\n 카드 기본값은 글이 많은 목록(직원·결재함)에 맞춰져 있다. 여기서는 사람이 먼저 보는 것이\n 형상 그림이므로, 그림 칸을 비율로 잡아 카드 폭이 달라져도 그림이 나란히 읽히게 한다.\n\n 그림은 카드 끝까지 닿는다 — 위쪽 모서리만 카드와 같은 6px 로 깎아 한 장처럼 보이게 하고,\n 그림 안에 얹히는 초안 표시·삭제는 figure-thumb 이 그린다.\n */\n ox-grist {\n --data-card-min-width: 240px;\n --data-card-record-card-border-radius: 6px;\n --data-card-record-card-padding: 0;\n --data-card-thumbnail-height: auto;\n --data-card-thumbnail-aspect: 4 / 3;\n --data-card-thumbnail-padding: 0;\n --data-card-thumbnail-radius: 6px 6px 0 0;\n --data-card-thumbnail-background: var(--md-sys-color-surface-container);\n\n /*\n 카드 기본 글꼴은 굵다(제목 bold 1.1em). 글이 정보를 나르는 목록에서는 맞지만, 여기서\n 정보를 나르는 것은 그림이다. 굵은 제목이 카드마다 서면 그림보다 글이 먼저 읽힌다 —\n 이름은 알아볼 정도로만, 타입은 곁글로 둔다.\n */\n --data-card-item-name-font: 500 0.9rem/1.35 var(--theme-font, 'Roboto');\n --data-card-item-name-color: var(--md-sys-color-on-surface);\n --data-card-item-disc-font: normal 0.72rem/1.2 var(--theme-font, 'Roboto');\n --data-card-item-disc-color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n --data-card-item-etc-font: normal 0.72rem/1.2 var(--theme-font, 'Roboto');\n\n /* 상태와 성숙도는 짧은 표식이다 — 두 줄로 세우지 않고 한 줄에 뉜다 */\n --data-card-detail-direction: row;\n\n /*\n 맨 위로 가는 화살표를 만들기 단추 **위로** 올린다. 둘 다 오른쪽 아래에 떠 있으므로\n 그냥 두면 겹친다 — 단추(36px + 아래 여백 12px) 위에 12px 을 띄운 자리다.\n */\n --data-card-fab-position-vertical: 64px;\n --data-list-fab-position-vertical: 64px;\n }\n `\n ]\n\n @property({ type: Object }) gristConfig: any\n\n @query('ox-grist') private grist!: DataGrist\n\n private listen?: () => void\n\n get context() {\n return {\n title: i18next.t('figure.title.figures'),\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || '',\n placeholder: i18next.t('figure.text.search-figure')\n },\n filter: {\n handler: () => {\n this.grist.toggleHeadroom()\n }\n }\n /*\n **만들기는 `actions` 가 아니라 FAB 이다.**\n\n `context.actions` 로 두면 페이지 바닥 줄에 글자 단추로 서는데, 그 줄은 「지금 화면에 대해\n 할 일」을 늘어놓는 자리다. 만들기는 이 화면의 유일한 주된 동작이므로 줄에 섞이지 않고\n 떠 있어야 한다. 카드를 가리지 않는 오른쪽 아래, 손이 가는 자리다(아래 render 의 [fab]).\n */\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n mode=\"CARD\"\n @search-tag=${(e: CustomEvent) => this.findByTag(e.detail.tag)}\n .config=${this.gristConfig}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n <ox-filters-form autofocus without-search></ox-filters-form>\n </div>\n </div>\n\n <ox-grist-personalizer slot=\"setting\"></ox-grist-personalizer>\n </ox-grist>\n\n <button fab title=${i18next.t('figure.button.new-figure')} @click=${() => this.openModeller()}>\n <md-icon>add</md-icon>${i18next.t('figure.button.new-figure')}\n </button>\n `\n }\n\n /**\n * 카드의 태그 알약을 눌렀다 — **그 태그로 목록을 거른다.**\n *\n * 검색창에 그 말을 채워 넣는다. 격자에 조건만 걸고 창을 비워 두면 저작자는 **왜 목록이\n * 줄었는지 모른 채** 보게 되고, 되돌리려면 무엇을 지워야 하는지도 알 수 없다.\n *\n * 서버가 `tags` 뿐 아니라 이름·타입·설명·분류를 함께 훑으므로, 그 말이 다른 칸에 든 도형도\n * 같이 온다. 태그만 정확히 거르는 것이 아니라 **찾기**다.\n */\n private findByTag(tag: string) {\n this.grist.searchText = tag\n /* 창의 값은 `context` 에서 나온다 — 다시 알려야 헤더가 새 값을 그린다. */\n this.updateContext()\n }\n\n async pageInitialized() {\n this.gristConfig = {\n /*\n **쪽 나누기는 선택이 아니다.** 격자에 가상 스크롤이 없어 받은 것을 전부 DOM 에 그린다.\n 카드 모드는 끝까지 내려가면 다음 쪽을 이어 붙이므로, 한 쪽이 작아도 목록은 계속 읽힌다.\n */\n pagination: { pages: [24, 48, 96] },\n\n /*\n 아직 아무것도 없을 때 하는 말. **이 자리가 이 화면에서 가장 중요하다** — 처음 온 사람은\n 여기서 이 도구가 무엇을 만드는 것인지 알게 된다. 조건이 걸려 비었을 때는 격자가 스스로\n 다른 말을 한다(「찾는 것이 없다」). 그 둘을 섞지 않는 것이 격자 쪽에 이미 들어 있다.\n */\n emptyNote: {\n icon: 'deployed_code',\n title: i18next.t('figure.text.no-figure-yet'),\n description: i18next.t('figure.text.what-a-figure-is')\n },\n\n list: {\n /* 그림 칸은 그 컬럼의 renderer 가 그린다 — 그림 없을 때의 안내까지 `figure-thumb` 안이다 */\n thumbnail: 'thumbnail',\n fields: ['name', 'type'],\n /*\n ── 무게를 모른 채 고르게 두지 않는다 ────────────────────────────────\n 이름과 그림만 실으면 저작자는 **이 형상이 무거운지 모른 채** 고른다. 그것을 저작면에\n 들어가서야 아는 것은 늦다. 그래서 격자로 옮기기 전 손 카드가 실었던 셋을 그대로 싣는다 —\n 몇 개까지 놓을 수 있나(배치 한도) · 성숙도 · 삼각형 수.\n\n 한 줄에 뉜다(`--data-card-detail-direction: row`). 이름표는 달지 않는다 — 값 자체가\n 「625개까지」·「삼각형 440개」로 읽히고, 성숙도만 숫자 하나여서 손이 갔을 때 이름이 뜬다.\n\n 상태는 이 줄에 없다. 그림 위 초안 표시가 그 일을 한다(`figure-thumb`) — 발행된 것에는\n 아무 말도 하지 않으므로 빈 칸이 생기지 않는다.\n */\n details: ['capacity', 'score', 'triangles']\n },\n\n columns: [\n { type: 'gutter', gutterName: 'sequence', fixed: true },\n {\n /*\n 표 모드에서만 쓰는 삭제 칸. 카드에서는 `figure-thumb` 이 그림 위에 그리므로\n `forList: false` 로 내린다 — 카드 아래 단추 줄은 감춰도 자리가 남는다.\n */\n type: 'gutter',\n gutterName: 'button',\n icon: 'delete',\n title: i18next.t('figure.button.delete-figure'),\n danger: true,\n forList: false,\n width: 40,\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: Figure) => this.removeFigure(record)\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('figure.label.name'),\n record: { editable: false },\n filter: 'search',\n sortable: true,\n width: 200\n },\n {\n /*\n 타입 이름은 **저장되는 식별자**다. 표시 이름과 다르게 보이게 두어야 도면이 무엇으로\n 이 형상을 찾는지 헷갈리지 않는다.\n */\n type: 'string',\n name: 'type',\n header: i18next.t('figure.label.figure-type'),\n record: {\n editable: false,\n renderer: (value: string) => html`<code>${value}</code>`\n },\n filter: 'search',\n sortable: true,\n width: 160\n },\n {\n /*\n 태그 — **카드에서 곁줄로 보이고 검색에 걸린다.**\n\n 서버가 이미 `type`·`name`·`description`·`category`·`tags` 를 함께 훑는다. 그런데\n 화면에는 태그가 어디에도 안 보여서, 저작자가 적은 태그가 **자기 눈에 돌아오지 않았다** —\n 적어 둔 것이 쓰이는지 알 수 없으면 아무도 적지 않는다.\n\n 정렬은 걸지 않는다. 배열이라 무엇을 기준으로 줄을 세우는지 답할 수 없다.\n\n **카드에서는 이 컬럼이 그리지 않는다.** 본문에 한 줄로 두었더니 태그가 있는 카드만\n 높아져 격자가 어긋났다 — 개수가 도형마다 다르므로 본문에 두는 한 남는 문제다.\n 카드에서는 `figure-thumb` 이 그림 위에 얹는다. 여기 남은 것은 표 모드의 칸이다.\n */\n type: 'string',\n name: 'tags',\n header: i18next.t('figure.label.tags'),\n record: {\n editable: false,\n renderer: (value: string[] | null) => (value || []).join(' · ')\n },\n filter: 'search',\n sortable: false,\n width: 180\n },\n {\n type: 'string',\n name: 'state',\n header: i18next.t('figure.label.state'),\n record: {\n editable: false,\n /*\n **발행된 것에는 아무 말도 하지 않는다.** 카탈로그에 있는 것은 대개 발행된 것이라,\n 카드마다 「발행됨」이 붙으면 그 말이 아무것도 가르지 않고 자리만 먹는다. 판 번호도\n 고를 때 쓰는 값이 아니다 — 이력 탭에 있다.\n\n 표 모드에서는 칸에 값이 있어야 하므로 그때는 발행됨도 적는다(`wide` 가 아니라 label\n 이 붙는 자리인지로 가른다 — 카드는 label 을 달지 않았다).\n */\n renderer: (value: string, _column: any, record: Figure, _rowIndex: number, field: Element) => {\n if (value !== 'released') {\n return html`<span style=${DRAFT}>${i18next.t('figure.label.draft')}</span>`\n }\n\n /* 표에서는 칸이 비면 안 된다. 카드에서만 입을 다문다 — 그리는 자리가 알려 준다. */\n return field?.tagName === 'OX-CARD-FIELD'\n ? ''\n : html`<span style=${RELEASED}\n >${i18next.t('figure.label.figure-released')}\n ${record.version ? i18next.t('figure.text.version-number', { version: record.version }) : ''}</span\n >`\n }\n },\n /*\n 전체·초안·발행됨을 단추로 고른다 — 값이 둘뿐인 조건을 펼침 목록에 넣지 않는다.\n\n 이름표는 비워 둔다. 단추에 「초안」·「발행됨」이라고 적혀 있으니 그 앞의 「상태」는\n 같은 말을 한 번 더 하는 것이고, 걸러 보기 줄에서 자리만 먹는다.\n */\n filter: {\n type: 'select-buttons',\n operator: 'in',\n label: '',\n options: [\n { display: i18next.t('figure.label.draft'), value: 'draft' },\n { display: i18next.t('figure.label.figure-released'), value: 'released' }\n ]\n },\n width: 120\n },\n {\n /*\n **몇 개까지 놓을 수 있나.** 카드에는 싣지 않고 표 모드의 칸으로 둔다 — 고른 뒤에\n 따지는 값이다. 서버에 없는 값이라 `groups` 와 `triangles` 에서 계산하는데, 저작면\n 머리와 **같은 함수**를 쓴다. 두 화면이 같은 형상을 다른 잣대로 말하면 무엇을 고칠지\n 모른다.\n */\n type: 'string',\n name: 'capacity',\n header: i18next.t('figure.label.placement-limit'),\n sortable: false,\n record: {\n editable: false,\n renderer: (_value: unknown, _column: any, record: Figure) => {\n const capacity = capacityFrom(record.groups ?? 0, record.triangles ?? 0)\n\n return capacity\n ? html`<span style=${CAPACITY} title=${i18next.t('figure.text.reuse-score-explained')}>\n ${i18next.t('figure.text.fits-n-instances', { n: capacity.instances.toLocaleString() })}\n </span>`\n : html`<span style=${UNMEASURED}>${i18next.t('figure.text.not-measured-yet')}</span>`\n }\n },\n width: 120\n },\n {\n type: 'float',\n name: 'score',\n header: i18next.t('figure.label.maturity'),\n record: {\n editable: false,\n /*\n 카드에 남긴 하나뿐인 숫자. **이름표를 달지 않으므로** 무엇인지는 손이 갔을 때 뜨는\n 말이 알려 준다.\n\n 색칠한 배지로 두지 않는다. 옛 draw call 한도 3 으로 재던 때는 카탈로그 거의 모두가 0 점이라\n 붉은 덩어리가 카드마다 섰고 아무것도 가르지 못했다. 이 모델은 「한 화면 N 대」 목표에 견준\n 여유라(`figureScoreOf`, 2026-09-24) 퍼지지만, 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.\n\n 아직 재지 않은 것을 0 으로 보여 주지 않는다 — 0 은 「가볍다」는 뜻이 되어 버린다.\n 등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` · `figureScoreOf` 가 정하고,\n 화면이 그것을 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 말한다.\n */\n renderer: (value: number | null) =>\n value == null\n ? html`<span style=${UNMEASURED}>${i18next.t('figure.text.not-measured-yet')}</span>`\n : html`<span style=${SCORE} title=${i18next.t('figure.label.maturity')}>\n ${value < ATTENTION_BELOW ? html`<i style=${LOW_DOT}></i>` : ''}${Math.round(value)}\n </span>`\n },\n sortable: true,\n width: 90\n },\n {\n type: 'integer',\n name: 'triangles',\n header: i18next.t('figure.label.triangles'),\n record: {\n editable: false,\n /*\n 카드에서는 이름표 없이 서므로 숫자만 두면 옆의 성숙도와 구별되지 않는다. 무엇의\n 수인지 값이 스스로 말하게 한다 — 표 모드에서는 머리글이 그 일을 하므로 겹치지만,\n 그 겹침이 카드에서 뜻을 잃는 것보다 낫다.\n */\n renderer: (value: number | null) =>\n value == null ? '' : i18next.t('figure.text.triangle-count', { count: value })\n },\n sortable: true,\n width: 110\n },\n {\n type: 'integer',\n name: 'groups',\n header: i18next.t('figure.label.budget-groups'),\n record: { editable: false },\n hidden: true,\n width: 0\n },\n {\n /*\n **카드의 그림 칸 전체를 이 renderer 가 그린다.**\n\n 기본 image renderer 는 그림만 그리고, 그 위에 얹혀야 하는 것(초안 표시·삭제)은 카드\n 아래 gutter 줄로 밀린다. 격자로 옮기기 전 손 카드가 그것을 그림 위에 두었고 그 편이\n 맞다 — 그래서 그림 칸을 `figure-thumb` 에게 넘긴다. hover 가 필요해 컴포넌트여야\n 하는 사연은 그 파일에 적혀 있다.\n\n `renderEmpty` — 그림이 아직 없는 것도 자리를 지킨다. 그 카드에도 초안 표시와 삭제가\n 있어야 하고, 「그림 없음」이라고 말하는 것도 `figure-thumb` 이 한다.\n */\n type: 'image',\n name: 'thumbnail',\n header: i18next.t('field.thumbnail'),\n record: {\n editable: false,\n align: 'center',\n wide: true,\n renderEmpty: true,\n renderer: (_value: unknown, _column: any, record: Figure) => html`\n <figure-thumb .figure=${record} @remove=${() => this.removeFigure(record)}></figure-thumb>\n `\n },\n hidden: true,\n width: 160\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated_at'),\n label: true,\n record: { editable: false },\n sortable: true,\n width: 180\n },\n {\n /*\n `resource-object` 를 쓰지 않는다 — 그 renderer 는 다른 패키지(resource-ui)가 등록하고,\n 등록되지 않은 앱에서는 조용히 글자 renderer 로 떨어져 `[object Object]` 가 된다.\n 이름 하나 꺼내는 일에 그 위험을 질 이유가 없다.\n */\n type: 'string',\n name: 'updater',\n header: i18next.t('field.updater'),\n record: {\n editable: false,\n renderer: (value: { name?: string } | null) => value?.name || ''\n },\n hidden: true,\n width: 120\n }\n ],\n\n rows: {\n selectable: false,\n appendable: false,\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: Figure) => {\n if (record?.id) {\n this.openModeller(record)\n }\n }\n }\n },\n\n sorters: [{ name: 'updatedAt', desc: true }]\n }\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n /*\n **로그인 정보가 도착하면 다시 불러온다.**\n\n 화면이 서는 것과 프로필이 오는 것은 순서가 정해져 있지 않고, 목록 질의는 로그인된 사람으로\n 나간다. 첫 질의가 프로필보다 앞서면 그 한 번이 실패하고, 그 뒤로 다시 묻는 사람이 없으면\n 화면은 비어 있는 채로 남는다. lite-menu 도 같은 신호로 같은 순서 문제를 푼다\n (`auth.on('profile')` — 그쪽 주석에 그 사연이 적혀 있다).\n */\n this.listen = () => {\n if (this.active) {\n this.grist?.fetch()\n }\n }\n auth.on('profile', this.listen)\n }\n\n disconnectedCallback() {\n if (this.listen) {\n auth.off?.('profile', this.listen)\n this.listen = undefined\n }\n super.disconnectedCallback()\n }\n\n async pageUpdated() {\n if (this.active) {\n this.grist.fetch()\n }\n }\n\n /**\n * 걸러 보기·정렬·쪽을 **격자가 만든 그대로** 서버에 넘긴다.\n *\n * 여기서 이름을 바꿔 담으면 격자가 아는 조건과 서버에 가는 조건이 어긋난다. 실패는 격자가\n * 받아 화면에 낸다 — 목록이 비어 있는 것과 못 불러온 것은 다른 사실이다.\n */\n async fetchHandler({ page = 1, limit = 24, sortings = [], filters = [] }: FetchOption) {\n const { items, total } = await fetchFigureList({ page, limit, filters, sortings })\n\n return { total: total || 0, records: items || [] }\n }\n\n /** The modeller that can edit this figure, by its format; a new figure starts in (`modeller-route.ts`). */\n private openModeller(figure?: Figure) {\n navigate(modellerPathOf(figure))\n }\n\n /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */\n private async removeFigure(figure: Figure) {\n // 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다\n const message = i18next.t('figure.text.delete-figure-confirm', { name: figure.name })\n if (!confirm(message)) {\n return\n }\n\n await deleteFigure(figure.id)\n this.grist.fetch()\n }\n}\n"]}
@@ -0,0 +1,102 @@
1
+ import { PageView } from '@operato/shell';
2
+ import '../modeller/editor.js';
3
+ import '@material/web/icon/icon.js';
4
+ import '../modeller/figure-history-panel.js';
5
+ import '../modeller/figure-verdict.js';
6
+ import '../modeller/figure-properties.js';
7
+ import '../modeller/figure-first-save.js';
8
+ /**
9
+ * The product page has exactly one authoritative workspace for UI and AI input transactions.
10
+ *
11
+ * The one modeller since ADR-0087 step 5 (2026-09-24): it answers to `figure-modeller`, and to `figure-modeller`
12
+ * as an alias for links made before the switch.
13
+ */
14
+ export declare class FigureModellerPage extends PageView {
15
+ static styles: import("lit").CSSResult;
16
+ private readonly session;
17
+ private routeId;
18
+ private discardApproved;
19
+ private revision;
20
+ /** The recipe templates (ADR-0095), read once when the page is first shown; none read, and the section is not drawn. */
21
+ private templates;
22
+ private templatesRead;
23
+ private guideOf;
24
+ private thumbnailOf;
25
+ private error;
26
+ /**
27
+ * 발행 패널이 읽는 행의 사실 — 상태와 발행 판 번호. 원본은 세션이 들고, 이 둘은 행에 있다.
28
+ *
29
+ * 이 화면에는 발행 패널이 없었다. 저장은 되고 발행 · 판본 · 복원 · 판정을 볼 자리가 없어서, ADR-0087 5단계에서
30
+ * `figure-modeller` 가 이 화면으로 바뀌면 저작자는 발행을 할 수 없었다. 새 도형은 공간을 선언하기 전에는 전부
31
+ * 발행 판정에 막히는데 그 선언을 할 단추도 없었다(2026-09-24 확인). 그래서 발행 패널을 건다.
32
+ */
33
+ private row;
34
+ private typeError;
35
+ /** Why a command from the side did nothing. Shown beside the tabs — the editor stays, since nothing in it was lost. */
36
+ private commandError;
37
+ /** The verdict's count, handed to the editor's verdict tab. */
38
+ private instances?;
39
+ private budget?;
40
+ /**
41
+ * The source the verdict judges. The session refuses to export while it loads or saves, so the last one read is
42
+ * kept rather than blanked — the verdict would otherwise flicker to 「waiting」 on every save.
43
+ */
44
+ private draft?;
45
+ private draftSource;
46
+ private wasDirty;
47
+ private titled?;
48
+ /** The figure the AI dock was last told about - another id is another conversation. */
49
+ private assistantFor?;
50
+ private provideAssistant;
51
+ private withdrawAssistant;
52
+ private changed;
53
+ private readRow;
54
+ /**
55
+ * 타입을 정한다 — 보드가 도형을 부르는 이름이고, 발행하면 굳는다(ADR-0063 결정 4).
56
+ *
57
+ * 새 도형은 저장할 때 임시 타입을 받는데, 이 화면에는 그것을 바꿀 칸이 없어서 임시 타입 그대로 발행됐다.
58
+ * 행만 바뀐다 — 자산은 타입을 원본에 적지 않으므로(적으면 형식이 거절한다) 편집 중인 원본과 부딪히지 않는다.
59
+ * 서버의 거절(이미 쓰는 타입 · 발행 뒤)은 그 문장 그대로 칸 아래에 둔다.
60
+ */
61
+ private renameType;
62
+ /** 「부품에 맞추기」 — 공간을 지금 부품에서 다시 선언한다. 저장은 저작자가 따로 한다. */
63
+ private fitToParts;
64
+ /** A command from the properties tab — rename, declare the room, the mounting face. */
65
+ private onCommand;
66
+ /**
67
+ * One step of the session's history, whichever part of the side asked for it — so undo takes it back like any
68
+ * other edit, and it is saved with the figure.
69
+ */
70
+ private runCommand;
71
+ /**
72
+ * Category and tags — the catalogue's, kept on the row, not in the asset. Written at once; the server's
73
+ * refusal is shown as it gave it.
74
+ */
75
+ private onCatalog;
76
+ private released;
77
+ /** 과거 판으로 되돌렸으면 서버의 원본이 바뀐 것이다 — 다시 연다. 패널은 저장 안 된 것이 있으면 되돌리기를 막는다. */
78
+ private reverted;
79
+ /**
80
+ * The dock's way in (`figure-assistant.ts`): input changes, or a proposal's commands as one step (ADR-0094). A
81
+ * revision is applied on the version the model saw; a new figure's proposal only on a new figure not yet touched. A
82
+ * page on a saved figure leaves a new figure's proposal untaken, and the handler opens a new figure for it.
83
+ */
84
+ private applyAI;
85
+ private holdUnsaved;
86
+ connectedCallback(): void;
87
+ disconnectedCallback(): void;
88
+ canDeactivate(): Promise<boolean>;
89
+ pageUpdated(changes: Record<string, unknown>, lifecycle: {
90
+ resourceId?: string;
91
+ }): Promise<void>;
92
+ /** A new figure the dock opened for a proposal: its steps, applied as the new figure's first step. */
93
+ private takeNewFigureProposal;
94
+ /** The first save's question, over the names this domain already uses. */
95
+ private askFirstSave;
96
+ get context(): {
97
+ title: string;
98
+ actions: never[];
99
+ heading: boolean;
100
+ };
101
+ render(): import("lit-html").TemplateResult<1>;
102
+ }