@things-factory/figure-ui 10.1.74 → 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.
- package/client/figure-assistant.ts +3 -3
- package/client/graphql/{v3-store.ts → store.ts} +6 -6
- package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
- package/client/modeller/{v3-align.ts → align.ts} +14 -14
- package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
- package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
- package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
- package/client/modeller/{v3-editor.ts → editor.ts} +137 -126
- package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
- package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
- package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
- package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
- package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
- package/client/modeller/figure-history-panel.ts +1 -1
- package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
- package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
- package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
- package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
- package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
- package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
- package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
- package/client/modeller/gate-findings.ts +2 -2
- package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
- package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
- package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
- package/client/modeller/{v3-links.ts → links.ts} +59 -59
- package/client/modeller/{v3-live.ts → live.ts} +9 -9
- package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
- package/client/modeller/modeller-route.ts +1 -1
- package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
- package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
- package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
- package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
- package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
- package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
- package/client/modeller/{v3-refusals.ts → refusals.ts} +62 -13
- package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
- package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
- package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
- package/client/route.ts +1 -5
- package/dist-client/figure-assistant.js +3 -3
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/store.d.ts +3 -0
- package/dist-client/graphql/store.js +39 -0
- package/dist-client/graphql/store.js.map +1 -0
- package/dist-client/modeller/ai-proposal.d.ts +51 -0
- package/dist-client/modeller/ai-proposal.js +64 -0
- package/dist-client/modeller/ai-proposal.js.map +1 -0
- package/dist-client/modeller/align.d.ts +30 -0
- package/dist-client/modeller/align.js +75 -0
- package/dist-client/modeller/align.js.map +1 -0
- package/dist-client/modeller/canvas.d.ts +72 -0
- package/dist-client/modeller/canvas.js +379 -0
- package/dist-client/modeller/canvas.js.map +1 -0
- package/dist-client/modeller/clips.d.ts +39 -0
- package/dist-client/modeller/clips.js +58 -0
- package/dist-client/modeller/clips.js.map +1 -0
- package/dist-client/modeller/controls.d.ts +28 -0
- package/dist-client/modeller/controls.js +28 -0
- package/dist-client/modeller/controls.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +203 -0
- package/dist-client/modeller/editor.js +1526 -0
- package/dist-client/modeller/editor.js.map +1 -0
- package/dist-client/modeller/fields.d.ts +45 -0
- package/dist-client/modeller/fields.js +83 -0
- package/dist-client/modeller/fields.js.map +1 -0
- package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
- package/dist-client/modeller/figure-capability-panel.js +141 -0
- package/dist-client/modeller/figure-capability-panel.js.map +1 -0
- package/dist-client/modeller/figure-clips.d.ts +31 -0
- package/dist-client/modeller/figure-clips.js +268 -0
- package/dist-client/modeller/figure-clips.js.map +1 -0
- package/dist-client/modeller/figure-controls.d.ts +36 -0
- package/dist-client/modeller/figure-controls.js +226 -0
- package/dist-client/modeller/figure-controls.js.map +1 -0
- package/dist-client/modeller/figure-facts.d.ts +35 -0
- package/dist-client/modeller/figure-facts.js +55 -0
- package/dist-client/modeller/figure-facts.js.map +1 -0
- package/dist-client/modeller/figure-first-save.d.ts +20 -0
- package/dist-client/modeller/figure-first-save.js +118 -0
- package/dist-client/modeller/figure-first-save.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +1 -1
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +77 -0
- package/dist-client/modeller/figure-links.js +649 -0
- package/dist-client/modeller/figure-links.js.map +1 -0
- package/dist-client/modeller/figure-material-panel.d.ts +39 -0
- package/dist-client/modeller/figure-material-panel.js +323 -0
- package/dist-client/modeller/figure-material-panel.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +69 -0
- package/dist-client/modeller/figure-parts.js +559 -0
- package/dist-client/modeller/figure-parts.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +34 -0
- package/dist-client/modeller/figure-preview.js +346 -0
- package/dist-client/modeller/figure-preview.js.map +1 -0
- package/dist-client/modeller/figure-properties.d.ts +45 -0
- package/dist-client/modeller/figure-properties.js +239 -0
- package/dist-client/modeller/figure-properties.js.map +1 -0
- package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
- package/dist-client/modeller/figure-recipe-catalog.js +399 -0
- package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +64 -0
- package/dist-client/modeller/figure-verdict.js +787 -0
- package/dist-client/modeller/figure-verdict.js.map +1 -0
- package/dist-client/modeller/first-save.d.ts +17 -0
- package/dist-client/modeller/first-save.js +26 -0
- package/dist-client/modeller/first-save.js.map +1 -0
- package/dist-client/modeller/gate-findings.d.ts +2 -2
- package/dist-client/modeller/gate-findings.js +2 -2
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +72 -0
- package/dist-client/modeller/gizmo.js +111 -0
- package/dist-client/modeller/gizmo.js.map +1 -0
- package/dist-client/modeller/help.d.ts +18 -0
- package/dist-client/modeller/help.js +37 -0
- package/dist-client/modeller/help.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +19 -0
- package/dist-client/modeller/joint-drag.js +97 -0
- package/dist-client/modeller/joint-drag.js.map +1 -0
- package/dist-client/modeller/joint-fan.d.ts +9 -0
- package/dist-client/modeller/joint-fan.js +276 -0
- package/dist-client/modeller/joint-fan.js.map +1 -0
- package/dist-client/modeller/keys.d.ts +22 -0
- package/dist-client/modeller/keys.js +26 -0
- package/dist-client/modeller/keys.js.map +1 -0
- package/dist-client/modeller/links.d.ts +267 -0
- package/dist-client/modeller/links.js +480 -0
- package/dist-client/modeller/links.js.map +1 -0
- package/dist-client/modeller/live.d.ts +37 -0
- package/dist-client/modeller/live.js +68 -0
- package/dist-client/modeller/live.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +120 -0
- package/dist-client/modeller/maturity.js +313 -0
- package/dist-client/modeller/maturity.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +1 -1
- package/dist-client/modeller/modeller-route.js +1 -1
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +18 -0
- package/dist-client/modeller/occupancy.js +24 -0
- package/dist-client/modeller/occupancy.js.map +1 -0
- package/dist-client/modeller/palette.d.ts +7 -0
- package/dist-client/modeller/palette.js +46 -0
- package/dist-client/modeller/palette.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +47 -0
- package/dist-client/modeller/preview-model.js +68 -0
- package/dist-client/modeller/preview-model.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +510 -0
- package/dist-client/modeller/product-session.js +157 -0
- package/dist-client/modeller/product-session.js.map +1 -0
- package/dist-client/modeller/recipe-source.d.ts +8 -0
- package/dist-client/modeller/recipe-source.js +14 -0
- package/dist-client/modeller/recipe-source.js.map +1 -0
- package/dist-client/modeller/recipes.d.ts +37 -0
- package/dist-client/modeller/recipes.js +103 -0
- package/dist-client/modeller/recipes.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +68 -0
- package/dist-client/modeller/refusals.js +142 -0
- package/dist-client/modeller/refusals.js.map +1 -0
- package/dist-client/modeller/renderer.d.ts +40 -0
- package/dist-client/modeller/renderer.js +73 -0
- package/dist-client/modeller/renderer.js.map +1 -0
- package/dist-client/modeller/stage-model.d.ts +39 -0
- package/dist-client/modeller/stage-model.js +147 -0
- package/dist-client/modeller/stage-model.js.map +1 -0
- package/dist-client/modeller/thumbnail.d.ts +3 -0
- package/dist-client/modeller/thumbnail.js +75 -0
- package/dist-client/modeller/thumbnail.js.map +1 -0
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +5 -0
- package/dist-client/modeller/v3-editor.js +21 -10
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +31 -0
- package/dist-client/modeller/v3-refusals.d.ts +37 -5
- package/dist-client/modeller/v3-refusals.js +59 -16
- package/dist-client/modeller/v3-refusals.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -1
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +102 -0
- package/dist-client/pages/figure-modeller-page.js +548 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +1 -5
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/product-cutover.md +25 -0
- package/package.json +5 -5
- package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
- package/test/{v3-align.test.ts → align.test.ts} +8 -8
- package/test/browser/modeller-page.js +30 -30
- package/test/browser/modeller.check.mjs +17 -16
- package/test/browser/serve.mjs +1 -1
- package/test/browser/thumbnail-page.js +6 -6
- package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
- package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
- package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
- package/test/figure-assistant-surface.test.ts +3 -3
- package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
- package/test/fixtures/joint-column.json +2 -2
- package/test/fixtures/joint-roller.json +2 -2
- package/test/fixtures/robot-arm-6dof.json +2 -2
- package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
- package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
- package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
- package/test/joints.ts +6 -6
- package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
- package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
- package/test/{v3-links.test.ts → links.test.ts} +201 -201
- package/test/{v3-live.test.ts → live.test.ts} +18 -18
- package/test/{v3-material.test.ts → material.test.ts} +12 -12
- package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
- package/test/modeller-route.test.ts +5 -6
- package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
- package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +11 -7
- package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
- package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
- package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
- package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
- package/test/{v3-recipes.test.ts → recipes.test.ts} +10 -10
- package/test/refusals.test.ts +166 -0
- package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
- package/test/renderer.test.ts +24 -0
- package/test/resolve-ts-siblings.mjs +1 -1
- package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
- package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
- package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
- package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
- package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
- package/test/{v3-view.test.ts → view.test.ts} +2 -2
- package/things-factory.config.js +2 -3
- package/translations/en.json +15 -7
- package/translations/ja.json +15 -7
- package/translations/ko.json +15 -7
- package/translations/ms.json +15 -7
- package/translations/zh.json +15 -7
- package/docs/v3-product-cutover.md +0 -25
- package/test/v3-refusals.test.ts +0 -136
- package/test/v3-renderer.test.ts +0 -24
- /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
- /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
- /package/client/modeller/{v3-help.ts → help.ts} +0 -0
- /package/client/modeller/{v3-keys.ts → keys.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":"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","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,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"]}
|
|
@@ -79,6 +79,11 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
79
79
|
protected willUpdate(_changed: PropertyValues): void;
|
|
80
80
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
81
81
|
private useGizmo;
|
|
82
|
+
/**
|
|
83
|
+
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
84
|
+
* (the network, the server) as it came.
|
|
85
|
+
*/
|
|
86
|
+
private said;
|
|
82
87
|
private run;
|
|
83
88
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
84
89
|
private drawnParts;
|
|
@@ -27,7 +27,7 @@ import { ScrollbarStyles } from '@operato/styles';
|
|
|
27
27
|
import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
|
|
28
28
|
import { paletteRecipe } from './v3-palette.js';
|
|
29
29
|
import { nudgeOf } from './v3-stage-model.js';
|
|
30
|
-
import { inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js';
|
|
30
|
+
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './v3-refusals.js';
|
|
31
31
|
import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js';
|
|
32
32
|
import { jointFanOf } from './v3-joint-fan.js';
|
|
33
33
|
import { jointDragOf } from './v3-joint-drag.js';
|
|
@@ -376,15 +376,24 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
376
376
|
this.renderRoot.querySelector('figure-v3-canvas')?.gizmo(mode);
|
|
377
377
|
this.gizmoMode = this.gizmoMode === mode ? undefined : mode;
|
|
378
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
381
|
+
* (the network, the server) as it came.
|
|
382
|
+
*/
|
|
383
|
+
said(error) {
|
|
384
|
+
return error?.code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error);
|
|
385
|
+
}
|
|
379
386
|
run(action) {
|
|
380
387
|
try {
|
|
381
388
|
action();
|
|
382
389
|
this.error = '';
|
|
390
|
+
/* The next step stands on its own: what the last removal let go of is no longer the news. */
|
|
391
|
+
this.notice = '';
|
|
383
392
|
this.changed();
|
|
384
393
|
}
|
|
385
394
|
catch (error) {
|
|
386
395
|
const say = (key, params) => i18next.t(key, params);
|
|
387
|
-
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ??
|
|
396
|
+
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? this.said(error);
|
|
388
397
|
}
|
|
389
398
|
finally {
|
|
390
399
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
@@ -456,7 +465,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
456
465
|
this.error = '';
|
|
457
466
|
}
|
|
458
467
|
catch (error) {
|
|
459
|
-
this.error =
|
|
468
|
+
this.error = this.said(error);
|
|
460
469
|
}
|
|
461
470
|
finally {
|
|
462
471
|
this.changed();
|
|
@@ -492,15 +501,17 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
492
501
|
removePart(id, refitRoom = false) {
|
|
493
502
|
this.retry = undefined;
|
|
494
503
|
try {
|
|
495
|
-
this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
|
|
504
|
+
const view = this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
|
|
496
505
|
this.error = '';
|
|
506
|
+
/* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
|
|
507
|
+
this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? '';
|
|
497
508
|
if (this.selected === id)
|
|
498
509
|
this.selected = '';
|
|
499
510
|
this.changed();
|
|
500
511
|
}
|
|
501
512
|
catch (error) {
|
|
502
513
|
/* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
|
|
503
|
-
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ??
|
|
514
|
+
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error);
|
|
504
515
|
/* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
|
|
505
516
|
if (!refitRoom && inDeclaredRoom(error))
|
|
506
517
|
this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) };
|
|
@@ -536,7 +547,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
536
547
|
this.changed();
|
|
537
548
|
}
|
|
538
549
|
catch (error) {
|
|
539
|
-
this.recipeError = error
|
|
550
|
+
this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error);
|
|
540
551
|
}
|
|
541
552
|
}
|
|
542
553
|
/** The design values of the asset being edited. */
|
|
@@ -629,7 +640,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
629
640
|
? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
|
|
630
641
|
: /is fastened to/.test(message)
|
|
631
642
|
? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
|
|
632
|
-
:
|
|
643
|
+
: this.said(error);
|
|
633
644
|
this.requestUpdate();
|
|
634
645
|
}
|
|
635
646
|
}
|
|
@@ -810,7 +821,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
810
821
|
? i18next.t('figure.text.fasten-would-loop')
|
|
811
822
|
: sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
|
|
812
823
|
placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
|
|
813
|
-
|
|
824
|
+
this.said(error);
|
|
814
825
|
}
|
|
815
826
|
}
|
|
816
827
|
changeField(field, event) {
|
|
@@ -1104,7 +1115,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1104
1115
|
this.changed();
|
|
1105
1116
|
}
|
|
1106
1117
|
catch (error) {
|
|
1107
|
-
this.error =
|
|
1118
|
+
this.error = this.said(error);
|
|
1108
1119
|
}
|
|
1109
1120
|
};
|
|
1110
1121
|
return html `<div public-pitch>
|
|
@@ -1162,7 +1173,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1162
1173
|
this.error =
|
|
1163
1174
|
code === 'INPUT_RANGE' && found
|
|
1164
1175
|
? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
|
|
1165
|
-
:
|
|
1176
|
+
: this.said(error);
|
|
1166
1177
|
}
|
|
1167
1178
|
}
|
|
1168
1179
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|