@things-factory/figure-ui 10.1.47 → 10.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/figure-v3-parts.ts +86 -11
- package/client/modeller/side-panel-styles.ts +8 -0
- package/client/modeller/v3-canvas.ts +62 -8
- package/client/modeller/v3-editor.ts +196 -24
- package/client/modeller/v3-fields.ts +36 -0
- package/client/modeller/v3-joint-fan.ts +292 -0
- package/client/modeller/v3-links.ts +8 -0
- package/client/modeller/v3-stage-model.ts +24 -0
- package/client/modeller/v3-thumbnail.ts +8 -4
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +13 -0
- package/dist-client/modeller/figure-v3-parts.js +81 -11
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/side-panel-styles.js +8 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/v3-canvas.d.ts +17 -0
- package/dist-client/modeller/v3-canvas.js +72 -8
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +34 -0
- package/dist-client/modeller/v3-editor.js +200 -22
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +15 -0
- package/dist-client/modeller/v3-fields.js +35 -0
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-joint-fan.d.ts +9 -0
- package/dist-client/modeller/v3-joint-fan.js +264 -0
- package/dist-client/modeller/v3-joint-fan.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +5 -0
- package/dist-client/modeller/v3-links.js +7 -0
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-stage-model.d.ts +8 -0
- package/dist-client/modeller/v3-stage-model.js +26 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -1
- package/dist-client/modeller/v3-thumbnail.js +6 -3
- package/dist-client/modeller/v3-thumbnail.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/v3-fields.test.ts +46 -1
- package/test/v3-gizmo.test.ts +3 -1
- package/test/v3-joint-fan.test.ts +210 -0
- package/test/v3-links.test.ts +13 -1
- package/test/v3-maturity.test.ts +1 -1
- package/test/v3-palette-icons.test.ts +22 -0
- package/test/v3-thumbnail.test.ts +29 -0
- package/translations/en.json +17 -2
- package/translations/ja.json +17 -2
- package/translations/ko.json +17 -2
- package/translations/ms.json +17 -2
- package/translations/zh.json +17 -2
|
@@ -5,6 +5,10 @@ import { i18next } from '@operato/i18n';
|
|
|
5
5
|
import { create, Model, niceGridStep } from '@hatiolab/things-scene';
|
|
6
6
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js';
|
|
7
7
|
import { boardFor, changedKeys, componentOf, gizmoResultOf } from './v3-stage-model.js';
|
|
8
|
+
import { JOINT_FAN } from './v3-joint-fan.js';
|
|
9
|
+
import { ownerOf } from './v3-fields.js';
|
|
10
|
+
/* The fan's colour when the figure's palette names no accent. */
|
|
11
|
+
const FAN_COLOUR = '#ff8a00';
|
|
8
12
|
/* The 2D side of the board, which the author never sees on this page but the scene paints. */
|
|
9
13
|
const PAPER = '#ffffff';
|
|
10
14
|
let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
@@ -18,6 +22,14 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
18
22
|
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
19
23
|
this.reach = { translate: false, rotate: false, scale: false };
|
|
20
24
|
this.view = DEFAULT_VIEW;
|
|
25
|
+
/**
|
|
26
|
+
* View only: nothing that happens on the board is told out. The card's picture is taken from a second canvas stood up
|
|
27
|
+
* off screen (`v3-thumbnail.ts`); the scene settles its components as it builds them, and a change told from there
|
|
28
|
+
* would reach the editor as an edit (V2's `viewOnly`, for the same reason).
|
|
29
|
+
*/
|
|
30
|
+
this.viewOnly = false;
|
|
31
|
+
/** Where the selected part turns and how far (`v3-joint-fan`): shown, never picked, never saved. */
|
|
32
|
+
this.fan = null;
|
|
21
33
|
/** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
|
|
22
34
|
this.writing = false;
|
|
23
35
|
}
|
|
@@ -56,7 +68,7 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
56
68
|
updated(changed) {
|
|
57
69
|
if (!this.hasUpdated)
|
|
58
70
|
return;
|
|
59
|
-
if (changed.has('parts') || changed.has('palette') || changed.has('context'))
|
|
71
|
+
if (changed.has('parts') || changed.has('palette') || changed.has('context') || changed.has('fan'))
|
|
60
72
|
this.standUp();
|
|
61
73
|
if (changed.has('view'))
|
|
62
74
|
this.applyView();
|
|
@@ -69,6 +81,26 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
69
81
|
this.watcher = undefined;
|
|
70
82
|
this.teardown();
|
|
71
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The board as a picture, for the card: the 3D view at this size, or nothing when it cannot be taken. The scene's
|
|
86
|
+
* `toDataURL` renders the 2D board (a white sheet, V2 found), and the 3D renderer keeps no drawing buffer, so the
|
|
87
|
+
* scene's own `snapshot3d` draws and reads in one task. A picture that cannot be taken is not an error and is not
|
|
88
|
+
* faked (V2's `snapshot`, lost when this canvas moved onto the scene -- no card had a picture since, user 2026-09-24).
|
|
89
|
+
*/
|
|
90
|
+
async snapshot(width = 480, height = 360) {
|
|
91
|
+
const layer = this.scene?.root;
|
|
92
|
+
if (!layer?.snapshot3d)
|
|
93
|
+
return undefined;
|
|
94
|
+
try {
|
|
95
|
+
/* webp: a card needs no alpha, and png is three or four times heavier in every list response. */
|
|
96
|
+
const url = await layer.snapshot3d(width, height, 'image/webp', 0.85);
|
|
97
|
+
return typeof url === 'string' && url.startsWith('data:') ? url : undefined;
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.warn(`[thumbnail] could not take the picture -- ${e.message}`);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
72
104
|
/** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */
|
|
73
105
|
fit() {
|
|
74
106
|
this.scene?.fit?.('ratio');
|
|
@@ -83,7 +115,13 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
83
115
|
return;
|
|
84
116
|
const needed = boardFor(this.parts, this.board);
|
|
85
117
|
const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth;
|
|
86
|
-
if (this.scene && this.standingFor === this.context
|
|
118
|
+
if (this.scene && this.standingFor === this.context) {
|
|
119
|
+
/*
|
|
120
|
+
A part dragged past the board's edge grows the board where it stands. Standing it up again lost the selection,
|
|
121
|
+
the gizmo and the camera after every such drag -- the gizmo seemed not to work at all (user, 2026-09-24).
|
|
122
|
+
*/
|
|
123
|
+
if (outgrown)
|
|
124
|
+
this.grow(needed);
|
|
87
125
|
this.sync();
|
|
88
126
|
return;
|
|
89
127
|
}
|
|
@@ -122,7 +160,10 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
122
160
|
}
|
|
123
161
|
}
|
|
124
162
|
components() {
|
|
125
|
-
|
|
163
|
+
const parts = this.parts.map(part => componentOf(part, this.palette, this.board)).filter((c) => !!c);
|
|
164
|
+
/* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
|
|
165
|
+
const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board) : null;
|
|
166
|
+
return fan ? [...parts, { ...fan, capturable: false, locked: true }] : parts;
|
|
126
167
|
}
|
|
127
168
|
/** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */
|
|
128
169
|
standing() {
|
|
@@ -134,6 +175,20 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
134
175
|
}
|
|
135
176
|
return found;
|
|
136
177
|
}
|
|
178
|
+
/** A larger board under the same scene: its size, and the grid that suits it. Components are re-placed by `sync`. */
|
|
179
|
+
grow(needed) {
|
|
180
|
+
const layer = this.scene?.root;
|
|
181
|
+
this.board = needed;
|
|
182
|
+
if (!layer)
|
|
183
|
+
return;
|
|
184
|
+
this.writing = true;
|
|
185
|
+
try {
|
|
186
|
+
layer.set({ width: needed.width, height: needed.height, depth: needed.depth, gridStep: niceGridStep(Math.max(needed.width, needed.height)) });
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
this.writing = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
137
192
|
/** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */
|
|
138
193
|
sync() {
|
|
139
194
|
const layer = this.scene?.root;
|
|
@@ -171,12 +226,12 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
171
226
|
watchScene() {
|
|
172
227
|
const layer = this.scene?.root;
|
|
173
228
|
const root = layer?.root;
|
|
174
|
-
if (!layer || !root?.on)
|
|
229
|
+
if (!layer || !root?.on || this.viewOnly)
|
|
175
230
|
return;
|
|
176
231
|
root.on('selected', (after) => {
|
|
177
232
|
const name = after?.[0]?.state?.name;
|
|
178
233
|
const id = typeof name === 'string' ? name : '';
|
|
179
|
-
if (id !== this.selected)
|
|
234
|
+
if (ownerOf(id) !== this.selected)
|
|
180
235
|
this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }));
|
|
181
236
|
});
|
|
182
237
|
/*
|
|
@@ -204,11 +259,12 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
204
259
|
const root = layer?.root;
|
|
205
260
|
if (!layer || !root)
|
|
206
261
|
return;
|
|
207
|
-
|
|
262
|
+
/* A repeat is selected whole: every copy of it (the editor selects the part the author made). */
|
|
263
|
+
const wanted = this.selected ? [...this.standing()].filter(([name]) => ownerOf(name) === this.selected).map(([, component]) => component) : [];
|
|
208
264
|
const now = root.selected ?? [];
|
|
209
|
-
if (
|
|
265
|
+
if (now.length === wanted.length && wanted.every((one, i) => now[i] === one))
|
|
210
266
|
return;
|
|
211
|
-
root.selected =
|
|
267
|
+
root.selected = wanted;
|
|
212
268
|
layer.invalidate?.();
|
|
213
269
|
}
|
|
214
270
|
/** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */
|
|
@@ -250,6 +306,14 @@ __decorate([
|
|
|
250
306
|
property({ attribute: false }),
|
|
251
307
|
__metadata("design:type", Object)
|
|
252
308
|
], FigureV3Canvas.prototype, "view", void 0);
|
|
309
|
+
__decorate([
|
|
310
|
+
property({ type: Boolean }),
|
|
311
|
+
__metadata("design:type", Object)
|
|
312
|
+
], FigureV3Canvas.prototype, "viewOnly", void 0);
|
|
313
|
+
__decorate([
|
|
314
|
+
property({ attribute: false }),
|
|
315
|
+
__metadata("design:type", Object)
|
|
316
|
+
], FigureV3Canvas.prototype, "fan", void 0);
|
|
253
317
|
__decorate([
|
|
254
318
|
query('div[stage]'),
|
|
255
319
|
__metadata("design:type", HTMLDivElement)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3-canvas.js","sourceRoot":"","sources":["../../client/modeller/v3-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { create, Model, niceGridStep } from '@hatiolab/things-scene'\n\nimport type { GizmoReach } from './v3-gizmo.js'\nimport type { V3RenderPart } from './v3-renderer.js'\nimport { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'\nimport { boardFor, changedKeys, componentOf, gizmoResultOf, type Board } from './v3-stage-model.js'\n\n/*\n * The V3 modeller's canvas -- a things-scene board in editing mode, as the V2 modeller's was.\n *\n * Each body figure-model evaluates stands on the board as a `figure-v3-part` component, so everything the V2 canvas\n * had comes from the scene, not from code of our own: picking and the orange outline, the gizmo with its snapping and\n * live readout, box and shift selection, the camera (damping, zoom toward the cursor, double-click to fit, the six\n * view presets and their keys), the studio environment, the coordinate ground, lights and shadows, supersampling.\n * An earlier canvas drew the bodies with three.js directly and lost all of that (user, 2026-09-24: \"why nerf it?\").\n *\n * What the gizmo writes back to a component comes out as `part-transformed` in the inputs' terms (`v3-stage-model`),\n * and the editor turns it into one step of the history; the next evaluation puts the component where the inputs say,\n * so a change a part cannot take (a fastened part moved) springs back.\n */\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown>) => void\n dispose?: () => void\n}\ninterface SceneLayer extends SceneComponent {\n components?: SceneComponent[]\n root?: { on?: (event: string, handler: (...args: unknown[]) => void) => void; selected?: SceneComponent[] }\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n addComponent?: (component: SceneComponent) => void\n removeComponent?: (component: SceneComponent) => void\n invalidate?: () => void\n fitCameraToModel?: () => void\n app?: unknown\n}\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: SceneLayer\n}\n\n/* The 2D side of the board, which the author never sees on this page but the scene paints. */\nconst PAPER = '#ffffff'\n\n@customElement('figure-v3-canvas')\nexport class FigureV3Canvas extends LitElement {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n min-height: 0;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n /* The scene sets position: relative inline on its target; the size is given so it still fills the host. */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n margin: 0;\n text-align: center;\n pointer-events: none;\n color: #f4f4f4;\n }\n `\n @property({ attribute: false }) parts: V3RenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n /** Which figure is open. Another one stands the board up again. */\n @property() context = ''\n @property() selected = ''\n /** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */\n @property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }\n @property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n private scene?: SceneHandle\n private board?: Board & { depth: number }\n private standingFor?: string\n private watcher?: ResizeObserver\n /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */\n private writing = false\n\n render() {\n return html`<div stage></div>${this.parts.length ? nothing : html`<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n protected updated(changed: PropertyValues) {\n if (!this.hasUpdated) return\n if (changed.has('parts') || changed.has('palette') || changed.has('context')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('selected')) this.showSelected()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */\n fit() {\n this.scene?.fit?.('ratio')\n this.scene?.root?.fitCameraToModel?.()\n }\n\n /**\n * Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or\n * one that has outgrown it -- standing up again loses the selection and the camera.\n */\n private standUp() {\n if (!this.stage) return\n const needed = boardFor(this.parts, this.board)\n const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth\n if (this.scene && this.standingFor === this.context && !outgrown) {\n this.sync()\n return\n }\n this.teardown()\n if (!this.parts.length) return\n this.board = needed\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: needed.width,\n height: needed.height,\n depth: needed.depth,\n threed: true,\n /* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large (V2). */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n ...sceneLook(this.view ?? DEFAULT_VIEW),\n gridStep: niceGridStep(Math.max(needed.width, needed.height)),\n sky: 'color',\n fillStyle: PAPER,\n components: this.components()\n },\n /* Editing mode: the camera view buttons, gizmo modes, help and keys V2's canvas had. */\n mode: 1\n }) as SceneHandle\n this.standingFor = this.context\n this.watchScene()\n this.scene.resize?.()\n this.fit()\n this.showSelected()\n } catch (error) {\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n\n private components(): Record<string, unknown>[] {\n return this.parts.map(part => componentOf(part, this.palette, this.board!)).filter((c): c is Record<string, unknown> => !!c)\n }\n\n /** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */\n private standing(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */\n private sync() {\n const layer = this.scene?.root\n if (!layer || !this.board) return\n const wanted = new Map(this.components().map(c => [c.name as string, c]))\n const standing = this.standing()\n this.writing = true\n try {\n for (const [name, component] of standing) {\n if (wanted.has(name)) continue\n layer.removeComponent?.(component)\n component.dispose?.()\n }\n for (const [name, state] of wanted) {\n const found = standing.get(name)\n if (found) {\n const diff = changedKeys(found.state, state)\n if (diff) found.set(diff)\n continue\n }\n const made = Model.compile({ ...state }, layer.app as never) as SceneComponent | null\n if (made) layer.addComponent?.(made)\n }\n } finally {\n this.writing = false\n }\n layer.invalidate?.()\n }\n\n /** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */\n private watchScene() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root?.on) return\n root.on('selected', (after: unknown) => {\n const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name\n const id = typeof name === 'string' ? name : ''\n if (id !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))\n })\n /*\n A change comes up to the board with the component in the last argument's `origin` (the scene appends\n { origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing (V2).\n */\n layer.on?.('change', (...args: unknown[]) => {\n if (this.writing) return\n const origin = (args[args.length - 1] as { origin?: SceneComponent } | undefined)?.origin\n const name = origin?.state?.name\n const part = typeof name === 'string' ? this.parts.find(p => p.id === name) : undefined\n if (!part || !this.board) return\n const result = gizmoResultOf(part, origin!.state, this.board)\n if (!result.position && !result.rotation && !result.scale) return\n this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }))\n /* After the editor has applied (or refused) it: a refused change springs back to where the inputs put it. */\n setTimeout(() => this.sync())\n })\n }\n\n private showSelected() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root) return\n const component = this.selected ? this.standing().get(this.selected) : undefined\n const now = root.selected ?? []\n if ((now[0] ?? undefined) === component && now.length === (component ? 1 : 0)) return\n root.selected = component ? [component] : []\n layer.invalidate?.()\n }\n\n /** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */\n private applyView() {\n const layer = this.scene?.root\n if (!layer) return\n layer.set(sceneLook(this.view ?? DEFAULT_VIEW))\n layer.invalidate?.()\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standingFor = undefined\n this.board = undefined\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"v3-canvas.js","sourceRoot":"","sources":["../../client/modeller/v3-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { create, Model, niceGridStep } from '@hatiolab/things-scene'\n\nimport type { GizmoReach } from './v3-gizmo.js'\nimport type { V3RenderPart } from './v3-renderer.js'\nimport { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'\nimport { boardFor, changedKeys, componentOf, gizmoResultOf, type Board } from './v3-stage-model.js'\nimport { JOINT_FAN } from './v3-joint-fan.js'\nimport { ownerOf } from './v3-fields.js'\n\n/*\n * The V3 modeller's canvas -- a things-scene board in editing mode, as the V2 modeller's was.\n *\n * Each body figure-model evaluates stands on the board as a `figure-v3-part` component, so everything the V2 canvas\n * had comes from the scene, not from code of our own: picking and the orange outline, the gizmo with its snapping and\n * live readout, box and shift selection, the camera (damping, zoom toward the cursor, double-click to fit, the six\n * view presets and their keys), the studio environment, the coordinate ground, lights and shadows, supersampling.\n * An earlier canvas drew the bodies with three.js directly and lost all of that (user, 2026-09-24: \"why nerf it?\").\n *\n * What the gizmo writes back to a component comes out as `part-transformed` in the inputs' terms (`v3-stage-model`),\n * and the editor turns it into one step of the history; the next evaluation puts the component where the inputs say,\n * so a change a part cannot take (a fastened part moved) springs back.\n */\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown>) => void\n dispose?: () => void\n}\ninterface SceneLayer extends SceneComponent {\n components?: SceneComponent[]\n root?: { on?: (event: string, handler: (...args: unknown[]) => void) => void; selected?: SceneComponent[] }\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n addComponent?: (component: SceneComponent) => void\n removeComponent?: (component: SceneComponent) => void\n invalidate?: () => void\n fitCameraToModel?: () => void\n app?: unknown\n}\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: SceneLayer\n}\n\n/* The fan's colour when the figure's palette names no accent. */\nconst FAN_COLOUR = '#ff8a00'\n\n/* The 2D side of the board, which the author never sees on this page but the scene paints. */\nconst PAPER = '#ffffff'\n\n@customElement('figure-v3-canvas')\nexport class FigureV3Canvas extends LitElement {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n min-height: 0;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n /* The scene sets position: relative inline on its target; the size is given so it still fills the host. */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n margin: 0;\n text-align: center;\n pointer-events: none;\n color: #f4f4f4;\n }\n `\n @property({ attribute: false }) parts: V3RenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n /** Which figure is open. Another one stands the board up again. */\n @property() context = ''\n @property() selected = ''\n /** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */\n @property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }\n @property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW\n /**\n * View only: nothing that happens on the board is told out. The card's picture is taken from a second canvas stood up\n * off screen (`v3-thumbnail.ts`); the scene settles its components as it builds them, and a change told from there\n * would reach the editor as an edit (V2's `viewOnly`, for the same reason).\n */\n @property({ type: Boolean }) viewOnly = false\n /** Where the selected part turns and how far (`v3-joint-fan`): shown, never picked, never saved. */\n @property({ attribute: false }) fan: V3RenderPart | null = null\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n private scene?: SceneHandle\n private board?: Board & { depth: number }\n private standingFor?: string\n private watcher?: ResizeObserver\n /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */\n private writing = false\n\n render() {\n return html`<div stage></div>${this.parts.length ? nothing : html`<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n protected updated(changed: PropertyValues) {\n if (!this.hasUpdated) return\n if (changed.has('parts') || changed.has('palette') || changed.has('context') || changed.has('fan')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('selected')) this.showSelected()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * The board as a picture, for the card: the 3D view at this size, or nothing when it cannot be taken. The scene's\n * `toDataURL` renders the 2D board (a white sheet, V2 found), and the 3D renderer keeps no drawing buffer, so the\n * scene's own `snapshot3d` draws and reads in one task. A picture that cannot be taken is not an error and is not\n * faked (V2's `snapshot`, lost when this canvas moved onto the scene -- no card had a picture since, user 2026-09-24).\n */\n async snapshot(width = 480, height = 360): Promise<string | undefined> {\n const layer = this.scene?.root as (SceneLayer & { snapshot3d?: (w?: number, h?: number, type?: string, quality?: number) => Promise<string | undefined> }) | undefined\n if (!layer?.snapshot3d) return undefined\n try {\n /* webp: a card needs no alpha, and png is three or four times heavier in every list response. */\n const url = await layer.snapshot3d(width, height, 'image/webp', 0.85)\n return typeof url === 'string' && url.startsWith('data:') ? url : undefined\n } catch (e) {\n console.warn(`[thumbnail] could not take the picture -- ${(e as Error).message}`)\n return undefined\n }\n }\n\n /** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */\n fit() {\n this.scene?.fit?.('ratio')\n this.scene?.root?.fitCameraToModel?.()\n }\n\n /**\n * Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or\n * one that has outgrown it -- standing up again loses the selection and the camera.\n */\n private standUp() {\n if (!this.stage) return\n const needed = boardFor(this.parts, this.board)\n const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth\n if (this.scene && this.standingFor === this.context) {\n /*\n A part dragged past the board's edge grows the board where it stands. Standing it up again lost the selection,\n the gizmo and the camera after every such drag -- the gizmo seemed not to work at all (user, 2026-09-24).\n */\n if (outgrown) this.grow(needed)\n this.sync()\n return\n }\n this.teardown()\n if (!this.parts.length) return\n this.board = needed\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: needed.width,\n height: needed.height,\n depth: needed.depth,\n threed: true,\n /* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large (V2). */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n ...sceneLook(this.view ?? DEFAULT_VIEW),\n gridStep: niceGridStep(Math.max(needed.width, needed.height)),\n sky: 'color',\n fillStyle: PAPER,\n components: this.components()\n },\n /* Editing mode: the camera view buttons, gizmo modes, help and keys V2's canvas had. */\n mode: 1\n }) as SceneHandle\n this.standingFor = this.context\n this.watchScene()\n this.scene.resize?.()\n this.fit()\n this.showSelected()\n } catch (error) {\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n\n private components(): Record<string, unknown>[] {\n const parts = this.parts.map(part => componentOf(part, this.palette, this.board!)).filter((c): c is Record<string, unknown> => !!c)\n /* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */\n const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null\n return fan ? [...parts, { ...fan, capturable: false, locked: true }] : parts\n }\n\n /** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */\n private standing(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /** A larger board under the same scene: its size, and the grid that suits it. Components are re-placed by `sync`. */\n private grow(needed: Board & { depth: number }) {\n const layer = this.scene?.root\n this.board = needed\n if (!layer) return\n this.writing = true\n try {\n layer.set({ width: needed.width, height: needed.height, depth: needed.depth, gridStep: niceGridStep(Math.max(needed.width, needed.height)) })\n } finally {\n this.writing = false\n }\n }\n\n /** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */\n private sync() {\n const layer = this.scene?.root\n if (!layer || !this.board) return\n const wanted = new Map(this.components().map(c => [c.name as string, c]))\n const standing = this.standing()\n this.writing = true\n try {\n for (const [name, component] of standing) {\n if (wanted.has(name)) continue\n layer.removeComponent?.(component)\n component.dispose?.()\n }\n for (const [name, state] of wanted) {\n const found = standing.get(name)\n if (found) {\n const diff = changedKeys(found.state, state)\n if (diff) found.set(diff)\n continue\n }\n const made = Model.compile({ ...state }, layer.app as never) as SceneComponent | null\n if (made) layer.addComponent?.(made)\n }\n } finally {\n this.writing = false\n }\n layer.invalidate?.()\n }\n\n /** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */\n private watchScene() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root?.on || this.viewOnly) return\n root.on('selected', (after: unknown) => {\n const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name\n const id = typeof name === 'string' ? name : ''\n if (ownerOf(id) !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))\n })\n /*\n A change comes up to the board with the component in the last argument's `origin` (the scene appends\n { origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing (V2).\n */\n layer.on?.('change', (...args: unknown[]) => {\n if (this.writing) return\n const origin = (args[args.length - 1] as { origin?: SceneComponent } | undefined)?.origin\n const name = origin?.state?.name\n const part = typeof name === 'string' ? this.parts.find(p => p.id === name) : undefined\n if (!part || !this.board) return\n const result = gizmoResultOf(part, origin!.state, this.board)\n if (!result.position && !result.rotation && !result.scale) return\n this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }))\n /* After the editor has applied (or refused) it: a refused change springs back to where the inputs put it. */\n setTimeout(() => this.sync())\n })\n }\n\n private showSelected() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root) return\n /* A repeat is selected whole: every copy of it (the editor selects the part the author made). */\n const wanted = this.selected ? [...this.standing()].filter(([name]) => ownerOf(name) === this.selected).map(([, component]) => component) : []\n const now = root.selected ?? []\n if (now.length === wanted.length && wanted.every((one, i) => now[i] === one)) return\n root.selected = wanted\n layer.invalidate?.()\n }\n\n /** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */\n private applyView() {\n const layer = this.scene?.root\n if (!layer) return\n layer.set(sceneLook(this.view ?? DEFAULT_VIEW))\n layer.invalidate?.()\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standingFor = undefined\n this.board = undefined\n }\n}\n"]}
|
|
@@ -50,6 +50,11 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
50
50
|
private run;
|
|
51
51
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
52
52
|
private drawnParts;
|
|
53
|
+
/**
|
|
54
|
+
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
55
|
+
* fan follows the arm while a control is dragged. Its size comes from what the joint turns (`v3-joint-fan`).
|
|
56
|
+
*/
|
|
57
|
+
private fanOf;
|
|
53
58
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
54
59
|
private hold;
|
|
55
60
|
private changed;
|
|
@@ -76,6 +81,11 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
76
81
|
* A part's name — the V2 part name field. It renames every value the part owns, and what refers to them follows
|
|
77
82
|
* (figure-model's rename-part), as one step of the history.
|
|
78
83
|
*/
|
|
84
|
+
/**
|
|
85
|
+
* A repeated body is one of its repeat's copies and is named after it -- the list and this field read `roller 15`, not
|
|
86
|
+
* its path through the repeat (user, 2026-09-24: 「부품 이름 ???」). A copy has no name of its own to change.
|
|
87
|
+
*/
|
|
88
|
+
private renderRepeatNote;
|
|
79
89
|
private renamePart;
|
|
80
90
|
/** Size response, fastening and motion for the selected part (parity ④ ⑤ ⑥), as authoring commands. */
|
|
81
91
|
private renderLinks;
|
|
@@ -95,6 +105,30 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
95
105
|
private range?;
|
|
96
106
|
/** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
|
|
97
107
|
private renderInstanceRange;
|
|
108
|
+
/**
|
|
109
|
+
* How the figure grows. One ratio (`sizing: 'uniform'`) is said as it is; a figure with joints that has not declared
|
|
110
|
+
* it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
|
|
111
|
+
*/
|
|
112
|
+
private renderSizingRule;
|
|
113
|
+
/**
|
|
114
|
+
* The parts list, one row per part the author made: a repeat is one row with how many copies it makes now (V2 listed
|
|
115
|
+
* its parts the same way; user, 2026-09-24: the conveyor's sixteen rollers were sixteen rows).
|
|
116
|
+
*/
|
|
117
|
+
private partRows;
|
|
118
|
+
/** The detail level's part budget for the list's head, from figure-model's count (a repeat once). */
|
|
119
|
+
private partBudgetOf;
|
|
120
|
+
/**
|
|
121
|
+
* A repeat: which way its copies run, how many there are now, and how far apart -- V2's `repeat: { axis, pitch }`.
|
|
122
|
+
* The pitch of a row that fills its length is edited as a number (`set-repeat` makes it the part's own the first
|
|
123
|
+
* time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
|
|
124
|
+
*/
|
|
125
|
+
private renderRepeat;
|
|
126
|
+
/**
|
|
127
|
+
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
128
|
+
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
129
|
+
* that -- said with the numbers.
|
|
130
|
+
*/
|
|
131
|
+
private setPitch;
|
|
98
132
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
99
133
|
private choose;
|
|
100
134
|
private asset;
|
|
@@ -4,6 +4,7 @@ import { LitElement, html, css, nothing } from 'lit';
|
|
|
4
4
|
import { i18next } from '@operato/i18n';
|
|
5
5
|
import '@operato/layout';
|
|
6
6
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
|
+
import { PART_LIMIT, v3PartCountOf } from '@hatiolab/figure-model';
|
|
7
8
|
import { captureV3Thumbnail } from './v3-thumbnail.js';
|
|
8
9
|
import { drawableV3Parts } from './v3-renderer.js';
|
|
9
10
|
import './v3-canvas.js';
|
|
@@ -18,11 +19,13 @@ import { DEFAULT_VIEW } from './figure-view.js';
|
|
|
18
19
|
import './figure-v3-capability.js';
|
|
19
20
|
import { gizmoChanges, gizmoReachOf } from './v3-gizmo.js';
|
|
20
21
|
import { keyActionOf } from './v3-keys.js';
|
|
21
|
-
import { groupFields } from './v3-fields.js';
|
|
22
|
+
import { groupFields, ownerOf, partLabelOf, repeatOf } from './v3-fields.js';
|
|
22
23
|
import { SidePanelStyles } from './side-panel-styles.js';
|
|
23
24
|
import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
|
|
24
25
|
import { paletteRecipe } from './v3-palette.js';
|
|
25
|
-
import {
|
|
26
|
+
import { nudgeOf } from './v3-stage-model.js';
|
|
27
|
+
import { instanceRangeOf, rangeDiffers, turnsAboutAJoint } from './v3-links.js';
|
|
28
|
+
import { jointFanOf } from './v3-joint-fan.js';
|
|
26
29
|
import { isLive, livePartsOf, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js';
|
|
27
30
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
28
31
|
let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
@@ -279,7 +282,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
279
282
|
this.error = '';
|
|
280
283
|
this.hiddenParts = new Set();
|
|
281
284
|
}
|
|
282
|
-
if (view && !view.geometry.some(g => g.id === this.selected))
|
|
285
|
+
if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected))
|
|
283
286
|
this.selected = '';
|
|
284
287
|
}
|
|
285
288
|
run(action) {
|
|
@@ -299,7 +302,21 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
299
302
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
300
303
|
drawnParts(geometry, asset) {
|
|
301
304
|
const parts = (isLive(this.live) && asset ? livePartsOf(asset, this.live) : null) ?? drawableV3Parts(geometry ?? []);
|
|
302
|
-
return parts.filter(body => !this.hiddenParts.has(body.id));
|
|
305
|
+
return parts.filter(body => !this.hiddenParts.has(ownerOf(body.id)));
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
309
|
+
* fan follows the arm while a control is dragged. Its size comes from what the joint turns (`v3-joint-fan`).
|
|
310
|
+
*/
|
|
311
|
+
fanOf(asset) {
|
|
312
|
+
if (!asset || !this.selected || this.hiddenParts.has(this.selected))
|
|
313
|
+
return null;
|
|
314
|
+
try {
|
|
315
|
+
return jointFanOf(asset, this.selected, this.live.values);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
303
320
|
}
|
|
304
321
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
305
322
|
hold(values, edit) {
|
|
@@ -388,7 +405,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
388
405
|
/** What the gizmo may write for the selected part — read off the asset being edited. */
|
|
389
406
|
reachOf(id) {
|
|
390
407
|
const none = { translate: false, rotate: false, scale: false };
|
|
391
|
-
|
|
408
|
+
/* A repeat is moved and sized through its row, not by a gizmo on one copy. */
|
|
409
|
+
if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id))
|
|
392
410
|
return none;
|
|
393
411
|
try {
|
|
394
412
|
return gizmoReachOf(JSON.parse(this.session.source()), id);
|
|
@@ -402,10 +420,39 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
402
420
|
const view = this.session.read();
|
|
403
421
|
const sizes = Object.fromEntries(view.fields.map(field => [field.id, field.value]));
|
|
404
422
|
const changes = gizmoChanges(id, this.reachOf(id), result, sizes);
|
|
405
|
-
if (changes.length)
|
|
423
|
+
if (changes.length) {
|
|
406
424
|
this.run(() => this.session.apply(changes));
|
|
407
|
-
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
/*
|
|
428
|
+
A part whose place is worked out (a converted one) has no numbers for the gizmo to write: the drag is added beside
|
|
429
|
+
its rule (`nudge-part`). A part a joint or a fastening places, or a size that is worked out, is refused -- said in
|
|
430
|
+
words, not by the part silently springing back (user, 2026-09-24: 「기즈모 컨트롤도 전혀 작동이 안되는군」).
|
|
431
|
+
*/
|
|
432
|
+
const body = view.geometry.find(g => g.id === id);
|
|
433
|
+
if (!body)
|
|
434
|
+
return this.requestUpdate();
|
|
435
|
+
if (result.scale && !result.position && !result.rotation) {
|
|
436
|
+
this.error = i18next.t('figure.text.gizmo-size-worked-out', { part: partLabelOf(id) });
|
|
437
|
+
return this.requestUpdate();
|
|
438
|
+
}
|
|
439
|
+
const { by, turn } = nudgeOf(body, result);
|
|
440
|
+
if (!Object.keys(by).length && !Object.keys(turn).length)
|
|
441
|
+
return this.requestUpdate();
|
|
442
|
+
try {
|
|
443
|
+
this.session.command({ kind: 'nudge-part', part: id, ...(Object.keys(by).length ? { by } : {}), ...(Object.keys(turn).length ? { turn } : {}) });
|
|
444
|
+
this.error = '';
|
|
445
|
+
this.changed();
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
449
|
+
this.error = /carried by a joint/.test(message)
|
|
450
|
+
? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
|
|
451
|
+
: /is fastened to/.test(message)
|
|
452
|
+
? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
|
|
453
|
+
: message;
|
|
408
454
|
this.requestUpdate();
|
|
455
|
+
}
|
|
409
456
|
}
|
|
410
457
|
/** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
|
|
411
458
|
align(tool) {
|
|
@@ -465,6 +512,14 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
465
512
|
* A part's name — the V2 part name field. It renames every value the part owns, and what refers to them follows
|
|
466
513
|
* (figure-model's rename-part), as one step of the history.
|
|
467
514
|
*/
|
|
515
|
+
/**
|
|
516
|
+
* A repeated body is one of its repeat's copies and is named after it -- the list and this field read `roller 15`, not
|
|
517
|
+
* its path through the repeat (user, 2026-09-24: 「부품 이름 ???」). A copy has no name of its own to change.
|
|
518
|
+
*/
|
|
519
|
+
renderRepeatNote(geometry) {
|
|
520
|
+
const repeat = repeatOf(this.selected, geometry);
|
|
521
|
+
return repeat?.index ? html `<p hint>${i18next.t('figure.text.repeat-member', { name: repeat.name, count: repeat.count, index: repeat.index })}</p>` : nothing;
|
|
522
|
+
}
|
|
468
523
|
renamePart(input) {
|
|
469
524
|
const name = input.value.trim();
|
|
470
525
|
const from = this.selected;
|
|
@@ -653,11 +708,133 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
653
708
|
<md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
|
|
654
709
|
</button>`
|
|
655
710
|
: nothing}
|
|
711
|
+
${this.renderSizingRule(asset, unavailable)}
|
|
712
|
+
</section>`;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* How the figure grows. One ratio (`sizing: 'uniform'`) is said as it is; a figure with joints that has not declared
|
|
716
|
+
* it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
|
|
717
|
+
*/
|
|
718
|
+
renderSizingRule(asset, unavailable) {
|
|
719
|
+
if (asset.sizing === 'uniform')
|
|
720
|
+
return html `<div inline-field><label>${i18next.t('figure.label.sizing-rule')}</label><span>${i18next.t('figure.label.sizing-uniform')}</span></div>`;
|
|
721
|
+
return html `${turnsAboutAJoint(asset) ? html `<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}</p>` : nothing}
|
|
722
|
+
<button act ?disabled=${unavailable} title=${i18next.t('figure.text.sizing-uniform-explained')} @click=${() => this.linkCommand({ kind: 'scale-uniformly' })}>
|
|
723
|
+
<md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
|
|
724
|
+
</button>`;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* The parts list, one row per part the author made: a repeat is one row with how many copies it makes now (V2 listed
|
|
728
|
+
* its parts the same way; user, 2026-09-24: the conveyor's sixteen rollers were sixteen rows).
|
|
729
|
+
*/
|
|
730
|
+
partRows(geometry) {
|
|
731
|
+
const rows = new Map();
|
|
732
|
+
for (const g of geometry) {
|
|
733
|
+
const id = ownerOf(g.id);
|
|
734
|
+
const row = rows.get(id);
|
|
735
|
+
if (row) {
|
|
736
|
+
row.copies = (row.copies ?? 1) + 1;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
rows.set(id, {
|
|
740
|
+
id,
|
|
741
|
+
provider: g.shape?.provider ?? '',
|
|
742
|
+
hidden: this.hiddenParts.has(id),
|
|
743
|
+
invisible: g.visible === false,
|
|
744
|
+
...(id !== g.id ? { copies: 1 } : {})
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return [...rows.values()];
|
|
748
|
+
}
|
|
749
|
+
/** The detail level's part budget for the list's head, from figure-model's count (a repeat once). */
|
|
750
|
+
partBudgetOf(asset) {
|
|
751
|
+
if (!asset)
|
|
752
|
+
return undefined;
|
|
753
|
+
try {
|
|
754
|
+
return { level: asset.detailLevel ?? null, used: v3PartCountOf(asset), limit: asset.detailLevel ? PART_LIMIT[asset.detailLevel] : null };
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* A repeat: which way its copies run, how many there are now, and how far apart -- V2's `repeat: { axis, pitch }`.
|
|
762
|
+
* The pitch of a row that fills its length is edited as a number (`set-repeat` makes it the part's own the first
|
|
763
|
+
* time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
|
|
764
|
+
*/
|
|
765
|
+
renderRepeat(asset, geometry, unavailable) {
|
|
766
|
+
const repeat = repeatOf(this.selected, geometry);
|
|
767
|
+
if (!asset || !repeat || repeat.index)
|
|
768
|
+
return nothing;
|
|
769
|
+
const model = asset.document.model;
|
|
770
|
+
const layout = model.nodes.find(n => n.id === `${this.selected}.layout`);
|
|
771
|
+
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
|
|
772
|
+
const own = `${this.selected}.repeat.pitch`;
|
|
773
|
+
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value;
|
|
774
|
+
const fits = layout?.op === 'fit-pitch@1';
|
|
775
|
+
/* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
|
|
776
|
+
const lengthRef = layout?.args[0] ?? '';
|
|
777
|
+
const length = asset.designInputs[lengthRef];
|
|
778
|
+
const longest = model.inputs.find(i => i.id === lengthRef)?.max;
|
|
779
|
+
const limit = model.constants.find(c => c.id === layout?.args[6])?.value;
|
|
780
|
+
const field = { id: own, unit: 'mm', role: 'design', value: pitch ?? 0 };
|
|
781
|
+
return html `<section>
|
|
782
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
783
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-axis')}</label><span value>${axis.toUpperCase()}</span></div>
|
|
784
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-count')}</label><span value>${repeat.count}</span></div>
|
|
785
|
+
<div inline-field>
|
|
786
|
+
<label>${i18next.t('figure.label.repeat-gap')} <span unit>mm</span></label>
|
|
787
|
+
<input
|
|
788
|
+
type="number"
|
|
789
|
+
step="any"
|
|
790
|
+
aria-label=${own}
|
|
791
|
+
.value=${String(pitch ?? '')}
|
|
792
|
+
?disabled=${unavailable || !fits}
|
|
793
|
+
@change=${(e) => this.setPitch(field, e)}
|
|
794
|
+
/>
|
|
795
|
+
</div>
|
|
796
|
+
<p hint>
|
|
797
|
+
${fits && length !== undefined && longest !== undefined && limit !== undefined
|
|
798
|
+
? i18next.t('figure.text.repeat-what-pitch-gives', {
|
|
799
|
+
length: length.toLocaleString(),
|
|
800
|
+
count: repeat.count,
|
|
801
|
+
longest: longest.toLocaleString(),
|
|
802
|
+
limit
|
|
803
|
+
})
|
|
804
|
+
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
805
|
+
</p>
|
|
656
806
|
</section>`;
|
|
657
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
810
|
+
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
811
|
+
* that -- said with the numbers.
|
|
812
|
+
*/
|
|
813
|
+
setPitch(field, event) {
|
|
814
|
+
const input = event.target;
|
|
815
|
+
const pitch = input.valueAsNumber;
|
|
816
|
+
if (!Number.isFinite(pitch) || pitch === field.value)
|
|
817
|
+
return;
|
|
818
|
+
try {
|
|
819
|
+
this.session.command({ kind: 'set-repeat', part: this.selected, pitch });
|
|
820
|
+
this.error = '';
|
|
821
|
+
this.changed();
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
input.value = String(field.value);
|
|
825
|
+
const code = error?.code;
|
|
826
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
827
|
+
const found = /\((\d[\d.]*) mm\) a pitch under ([\d.]+) mm makes more than (\d+)/.exec(message);
|
|
828
|
+
this.error =
|
|
829
|
+
code === 'INPUT_RANGE' && found
|
|
830
|
+
? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
|
|
831
|
+
: message;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
658
834
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
659
835
|
choose(id) {
|
|
660
|
-
|
|
836
|
+
/* A copy on the canvas selects its repeat: the part the author made. */
|
|
837
|
+
this.selected = id ? ownerOf(id) : id;
|
|
661
838
|
if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
|
|
662
839
|
this.tab = 'properties';
|
|
663
840
|
}
|
|
@@ -695,11 +872,12 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
695
872
|
if (!view)
|
|
696
873
|
return html `<p>${i18next.t('figure.text.opening-model')}</p>`;
|
|
697
874
|
const unavailable = view.loading || !!view.error || view.saving || !view.context;
|
|
698
|
-
const part = view.geometry.find(g => g.id === this.selected);
|
|
875
|
+
const part = view.geometry.find(g => ownerOf(g.id) === this.selected);
|
|
699
876
|
/* The controls (state inputs) have their own place in the motion tab; these are the design values. */
|
|
700
877
|
const design = view.fields.filter(i => i.role !== 'state');
|
|
701
|
-
|
|
702
|
-
const
|
|
878
|
+
/* A repeat's own row values (its pitch) have their own section, not the shape's. */
|
|
879
|
+
const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.') && !i.id.startsWith(this.selected + '.repeat.'));
|
|
880
|
+
const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(ownerOf(g.id) + '.')));
|
|
703
881
|
const status = view.loading
|
|
704
882
|
? 'figure.text.loading-figure'
|
|
705
883
|
: view.saving
|
|
@@ -733,18 +911,15 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
733
911
|
slot="start"
|
|
734
912
|
.budget=${this.budget}
|
|
735
913
|
@show-maturity=${() => (this.tab = 'verdict')}
|
|
736
|
-
.parts=${view.geometry
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
hidden: this.hiddenParts.has(g.id),
|
|
740
|
-
invisible: g.visible === false
|
|
741
|
-
}))}
|
|
914
|
+
.parts=${this.partRows(view.geometry)}
|
|
915
|
+
.partBudget=${this.partBudgetOf(asset)}
|
|
916
|
+
@set-detail-level=${(e) => this.linkCommand({ kind: 'set-detail-level', level: e.detail.level })}
|
|
742
917
|
.selected=${this.selected}
|
|
743
918
|
?locked=${unavailable}
|
|
744
919
|
@add-part=${(e) => this.add(e.detail.kind)}
|
|
745
920
|
@select-part=${(e) => this.choose(e.detail.id)}
|
|
746
921
|
@toggle-part-hidden=${(e) => this.toggleHiddenPart(e.detail.id)}
|
|
747
|
-
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => g.id)) : new Set())}
|
|
922
|
+
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => ownerOf(g.id))) : new Set())}
|
|
748
923
|
@move-part-up=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
749
924
|
@move-part-down=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
750
925
|
@duplicate-part=${(e) => this.duplicatePart(e.detail.id)}
|
|
@@ -760,6 +935,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
760
935
|
: html `<figure-v3-canvas
|
|
761
936
|
.view=${this.view}
|
|
762
937
|
.parts=${this.drawnParts(view.geometry, asset)}
|
|
938
|
+
.fan=${this.fanOf(asset)}
|
|
763
939
|
.palette=${view.palette ?? {}}
|
|
764
940
|
.context=${view.context ?? ''}
|
|
765
941
|
.selected=${this.selected}
|
|
@@ -790,16 +966,18 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
790
966
|
<input
|
|
791
967
|
name-field
|
|
792
968
|
aria-label=${i18next.t('figure.label.name')}
|
|
793
|
-
.value=${this.selected}
|
|
794
|
-
?disabled=${unavailable}
|
|
969
|
+
.value=${partLabelOf(this.selected)}
|
|
970
|
+
?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)}
|
|
795
971
|
@change=${(e) => this.renamePart(e.target)}
|
|
796
972
|
/>
|
|
797
973
|
</div>
|
|
974
|
+
${this.renderRepeatNote(view.geometry)}
|
|
798
975
|
</section>
|
|
799
|
-
${this.
|
|
976
|
+
${this.renderRepeat(asset, view.geometry, unavailable)}
|
|
977
|
+
${this.renderGroups(groupFields(own, this.selected), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable))}
|
|
800
978
|
${this.renderLinks(unavailable, 'shape')}
|
|
801
979
|
<figure-v3-material
|
|
802
|
-
.body=${part}
|
|
980
|
+
.body=${{ ...part, id: this.selected }}
|
|
803
981
|
.palette=${view.palette ?? {}}
|
|
804
982
|
?locked=${unavailable}
|
|
805
983
|
@v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
|