@things-factory/figure-ui 10.1.48 → 10.1.50
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/graphql/v3-store.ts +2 -1
- package/client/modeller/figure-history-panel.ts +9 -1
- package/client/modeller/figure-v3-first-save.ts +98 -0
- package/client/modeller/figure-v3-links.ts +89 -26
- package/client/modeller/figure-v3-parts.ts +86 -10
- package/client/modeller/figure-v3-properties.ts +2 -1
- package/client/modeller/side-panel-styles.ts +8 -0
- package/client/modeller/v3-canvas.ts +86 -12
- package/client/modeller/v3-editor.ts +244 -37
- package/client/modeller/v3-fields.ts +15 -1
- package/client/modeller/v3-first-save.ts +34 -0
- package/client/modeller/v3-gizmo.ts +52 -3
- package/client/modeller/v3-joint-drag.ts +116 -0
- package/client/modeller/v3-joint-fan.ts +24 -11
- package/client/modeller/v3-keys.ts +12 -1
- package/client/modeller/v3-links.ts +72 -5
- package/client/modeller/v3-live.ts +11 -0
- package/client/modeller/v3-product-session.ts +4 -2
- package/client/modeller/v3-refusals.ts +21 -0
- package/client/modeller/v3-stage-model.ts +24 -0
- package/client/pages/figure-v3-modeller-page.ts +46 -7
- package/dist-client/graphql/v3-store.js +2 -1
- package/dist-client/graphql/v3-store.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +9 -1
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-v3-first-save.d.ts +20 -0
- package/dist-client/modeller/figure-v3-first-save.js +118 -0
- package/dist-client/modeller/figure-v3-first-save.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +5 -0
- package/dist-client/modeller/figure-v3-links.js +81 -23
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +13 -0
- package/dist-client/modeller/figure-v3-parts.js +81 -10
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.js +2 -1
- package/dist-client/modeller/figure-v3-properties.js.map +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 +13 -0
- package/dist-client/modeller/v3-canvas.js +88 -12
- 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 +32 -1
- package/dist-client/modeller/v3-editor.js +245 -35
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +6 -0
- package/dist-client/modeller/v3-fields.js +14 -2
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-first-save.d.ts +17 -0
- package/dist-client/modeller/v3-first-save.js +27 -0
- package/dist-client/modeller/v3-first-save.js.map +1 -0
- package/dist-client/modeller/v3-gizmo.d.ts +18 -0
- package/dist-client/modeller/v3-gizmo.js +45 -2
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-joint-drag.d.ts +19 -0
- package/dist-client/modeller/v3-joint-drag.js +97 -0
- package/dist-client/modeller/v3-joint-drag.js.map +1 -0
- package/dist-client/modeller/v3-joint-fan.js +24 -11
- package/dist-client/modeller/v3-joint-fan.js.map +1 -1
- package/dist-client/modeller/v3-keys.d.ts +9 -1
- package/dist-client/modeller/v3-keys.js +3 -0
- package/dist-client/modeller/v3-keys.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +45 -2
- package/dist-client/modeller/v3-links.js +52 -3
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +6 -0
- package/dist-client/modeller/v3-live.js +10 -0
- package/dist-client/modeller/v3-live.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +4 -1
- package/dist-client/modeller/v3-product-session.js +2 -2
- package/dist-client/modeller/v3-product-session.js.map +1 -1
- package/dist-client/modeller/v3-refusals.d.ts +4 -0
- package/dist-client/modeller/v3-refusals.js +21 -0
- package/dist-client/modeller/v3-refusals.js.map +1 -0
- 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/pages/figure-v3-modeller-page.d.ts +4 -0
- package/dist-client/pages/figure-v3-modeller-page.js +45 -7
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -5
- package/test/browser/build.mjs +62 -0
- package/test/browser/modeller-page.js +198 -0
- package/test/browser/modeller.check.mjs +334 -0
- package/test/browser/serve.mjs +43 -0
- package/test/joints.ts +33 -0
- package/test/v3-clips.test.ts +3 -2
- package/test/v3-controls.test.ts +3 -2
- package/test/v3-fields.test.ts +38 -1
- package/test/v3-first-save.test.ts +75 -0
- package/test/v3-gizmo.test.ts +40 -5
- package/test/v3-joint-drag.test.ts +93 -0
- package/test/v3-joint-fan.test.ts +16 -6
- package/test/v3-keys.test.ts +15 -2
- package/test/v3-links.test.ts +62 -14
- package/test/v3-live.test.ts +23 -4
- package/test/v3-maturity.test.ts +3 -7
- package/test/v3-refusals.test.ts +28 -0
- package/translations/en.json +43 -5
- package/translations/ja.json +42 -4
- package/translations/ko.json +42 -4
- package/translations/ms.json +42 -4
- package/translations/zh.json +42 -4
|
@@ -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,13 +19,17 @@ 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, partLabelOf, repeatOf } from './v3-fields.js';
|
|
22
|
+
import { groupFields, ownerOf, partLabelOf, repeatOf } from './v3-fields.js';
|
|
22
23
|
import { SidePanelStyles } from './side-panel-styles.js';
|
|
24
|
+
import { ScrollbarStyles } from '@operato/styles';
|
|
23
25
|
import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
|
|
24
26
|
import { paletteRecipe } from './v3-palette.js';
|
|
27
|
+
import { nudgeOf } from './v3-stage-model.js';
|
|
28
|
+
import { sizeRangeRefusalOf } from './v3-refusals.js';
|
|
25
29
|
import { instanceRangeOf, rangeDiffers, turnsAboutAJoint } from './v3-links.js';
|
|
26
30
|
import { jointFanOf } from './v3-joint-fan.js';
|
|
27
|
-
import {
|
|
31
|
+
import { jointDragOf } from './v3-joint-drag.js';
|
|
32
|
+
import { liveOverSession, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js';
|
|
28
33
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
29
34
|
let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
30
35
|
constructor() {
|
|
@@ -48,16 +53,26 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
48
53
|
/** A value in hand -- typed, held on a spinner, scrubbed, dragged -- drawn at once and recorded on release. */
|
|
49
54
|
this.live = NOTHING_LIVE;
|
|
50
55
|
this.lastContext = null;
|
|
51
|
-
/** The modeller's keys (`v3-keys.ts`): undo, redo, duplicate, remove, back to the whole figure. */
|
|
56
|
+
/** The modeller's keys (`v3-keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. */
|
|
52
57
|
this.onKey = (e) => {
|
|
53
58
|
const view = this.session?.read();
|
|
54
59
|
if (!view || view.loading || view.saving || !view.context || !this.isConnected)
|
|
55
60
|
return;
|
|
56
|
-
const
|
|
61
|
+
const canvas = this.renderRoot.querySelector('figure-v3-canvas');
|
|
62
|
+
const path = e.composedPath();
|
|
63
|
+
const action = keyActionOf({ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, target: path[0], inScene: !!canvas && path.includes(canvas) }, this.selected);
|
|
57
64
|
if (!action)
|
|
58
65
|
return;
|
|
59
66
|
e.preventDefault();
|
|
60
|
-
|
|
67
|
+
/*
|
|
68
|
+
The session's history is the only one. With the canvas focused the scene took Ctrl+Z as well, undoing on its own
|
|
69
|
+
board the last value the canvas had written -- which the canvas then told as a drag: an undo of a drag on a
|
|
70
|
+
converted part wrote the drag again (2026-09-25). Heard first (capture) and kept from the scene.
|
|
71
|
+
*/
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
if (action === 'translate' || action === 'rotate' || action === 'scale')
|
|
74
|
+
canvas?.gizmo(action);
|
|
75
|
+
else if (action === 'undo')
|
|
61
76
|
view.canUndo && this.run(() => this.session.undo());
|
|
62
77
|
else if (action === 'redo')
|
|
63
78
|
view.canRedo && this.run(() => this.session.redo());
|
|
@@ -70,7 +85,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
70
85
|
};
|
|
71
86
|
}
|
|
72
87
|
static { FigureV3Editor_1 = this; }
|
|
73
|
-
|
|
88
|
+
/* The operato scrollbar, as every scrolling panel of the app has it -- the side panel scrolled with the browser's own. */
|
|
89
|
+
static { this.styles = [ScrollbarStyles, SidePanelStyles, css `
|
|
74
90
|
:host {
|
|
75
91
|
display: flex;
|
|
76
92
|
flex-direction: column;
|
|
@@ -266,10 +282,10 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
266
282
|
}
|
|
267
283
|
connectedCallback() {
|
|
268
284
|
super.connectedCallback();
|
|
269
|
-
window.addEventListener('keydown', this.onKey);
|
|
285
|
+
window.addEventListener('keydown', this.onKey, true);
|
|
270
286
|
}
|
|
271
287
|
disconnectedCallback() {
|
|
272
|
-
window.removeEventListener('keydown', this.onKey);
|
|
288
|
+
window.removeEventListener('keydown', this.onKey, true);
|
|
273
289
|
super.disconnectedCallback();
|
|
274
290
|
}
|
|
275
291
|
willUpdate(_changed) {
|
|
@@ -280,7 +296,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
280
296
|
this.error = '';
|
|
281
297
|
this.hiddenParts = new Set();
|
|
282
298
|
}
|
|
283
|
-
if (view && !view.geometry.some(g => g.id === this.selected))
|
|
299
|
+
if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected))
|
|
284
300
|
this.selected = '';
|
|
285
301
|
}
|
|
286
302
|
run(action) {
|
|
@@ -290,7 +306,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
290
306
|
this.changed();
|
|
291
307
|
}
|
|
292
308
|
catch (error) {
|
|
293
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
309
|
+
this.error = sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? (error instanceof Error ? error.message : String(error));
|
|
294
310
|
}
|
|
295
311
|
finally {
|
|
296
312
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
@@ -299,8 +315,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
299
315
|
}
|
|
300
316
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
301
317
|
drawnParts(geometry, asset) {
|
|
302
|
-
const parts = (
|
|
303
|
-
return parts.filter(body => !this.hiddenParts.has(body.id));
|
|
318
|
+
const parts = (asset ? liveOverSession(asset, this.currentInputs(), this.live) : null) ?? drawableV3Parts(geometry ?? []);
|
|
319
|
+
return parts.filter(body => !this.hiddenParts.has(ownerOf(body.id)));
|
|
304
320
|
}
|
|
305
321
|
/**
|
|
306
322
|
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
@@ -310,12 +326,21 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
310
326
|
if (!asset || !this.selected || this.hiddenParts.has(this.selected))
|
|
311
327
|
return null;
|
|
312
328
|
try {
|
|
313
|
-
return jointFanOf(asset, this.selected, this.live.values);
|
|
329
|
+
return jointFanOf(asset, this.selected, { ...this.currentInputs(), ...this.live.values });
|
|
314
330
|
}
|
|
315
331
|
catch {
|
|
316
332
|
return null;
|
|
317
333
|
}
|
|
318
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Every input as the session holds it now -- the pose the author has set with the controls, and design values. The
|
|
337
|
+
* asset's source carries only the saved defaults, so drawing a value in hand over it put every other joint back to its
|
|
338
|
+
* saved angle while one slider moved, and the arm jumped when released (user, 2026-09-24).
|
|
339
|
+
*/
|
|
340
|
+
currentInputs() {
|
|
341
|
+
const view = this.session?.read();
|
|
342
|
+
return view ? Object.fromEntries(view.fields.map(field => [field.id, field.value])) : {};
|
|
343
|
+
}
|
|
319
344
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
320
345
|
hold(values, edit) {
|
|
321
346
|
this.live = { values: { ...this.live.values, ...values }, ...(edit ? { edit } : this.live.edit ? { edit: this.live.edit } : {}) };
|
|
@@ -333,9 +358,21 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
333
358
|
not from the one on screen, which stands wherever the author left it turned. A picture that cannot
|
|
334
359
|
be taken is not an error: the save goes on without it (see `v3-thumbnail.ts`).
|
|
335
360
|
*/
|
|
361
|
+
let type;
|
|
362
|
+
if (!this.session.read().id && this.firstSave) {
|
|
363
|
+
const answer = await this.firstSave(this.session.read().name ?? '');
|
|
364
|
+
if (!answer)
|
|
365
|
+
return;
|
|
366
|
+
/* The name is the figure's own, in the asset: one step of the history, like typing it in the properties. */
|
|
367
|
+
if (answer.name !== this.session.read().name) {
|
|
368
|
+
this.session.command({ kind: 'rename', name: answer.name });
|
|
369
|
+
this.changed();
|
|
370
|
+
}
|
|
371
|
+
type = answer.type;
|
|
372
|
+
}
|
|
336
373
|
const view = this.session.read();
|
|
337
374
|
const thumbnail = await captureV3Thumbnail(drawableV3Parts(view.geometry ?? []), view.palette ?? {});
|
|
338
|
-
const promise = this.session.save({ thumbnail });
|
|
375
|
+
const promise = this.session.save({ thumbnail, type });
|
|
339
376
|
this.changed();
|
|
340
377
|
await promise;
|
|
341
378
|
this.error = '';
|
|
@@ -403,7 +440,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
403
440
|
/** What the gizmo may write for the selected part — read off the asset being edited. */
|
|
404
441
|
reachOf(id) {
|
|
405
442
|
const none = { translate: false, rotate: false, scale: false };
|
|
406
|
-
|
|
443
|
+
/* A repeat is moved and sized through its row, not by a gizmo on one copy. */
|
|
444
|
+
if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id))
|
|
407
445
|
return none;
|
|
408
446
|
try {
|
|
409
447
|
return gizmoReachOf(JSON.parse(this.session.source()), id);
|
|
@@ -415,12 +453,74 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
415
453
|
/** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
|
|
416
454
|
transformed(id, result) {
|
|
417
455
|
const view = this.session.read();
|
|
456
|
+
/* A part hanging from a joint: the drag edits the joint -- its value, or where it sits (ADR-0093). */
|
|
457
|
+
const asset = JSON.parse(this.session.source());
|
|
458
|
+
const drawn = view.geometry.find(g => g.id === id);
|
|
459
|
+
const drag = drawn ? jointDragOf(asset, ownerOf(id), drawn, result, this.currentInputs()) : null;
|
|
460
|
+
if (drag) {
|
|
461
|
+
if (drag.kind === 'value')
|
|
462
|
+
this.run(() => this.session.apply([{ kind: 'set-state', input: drag.input, unit: drag.unit, value: drag.value }]));
|
|
463
|
+
else if (drag.kind === 'joint')
|
|
464
|
+
this.run(() => this.session.command({ kind: 'set-joint', joint: drag.joint }));
|
|
465
|
+
else {
|
|
466
|
+
this.error = i18next.t(`figure.text.gizmo-joint-${drag.reason}`, { joint: drag.joint, leader: drag.leader ?? '', axis: drag.axis ?? '' });
|
|
467
|
+
this.requestUpdate();
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
418
471
|
const sizes = Object.fromEntries(view.fields.map(field => [field.id, field.value]));
|
|
419
|
-
|
|
420
|
-
|
|
472
|
+
/*
|
|
473
|
+
A part a joint places, scaled: only its measures change. The shift of its centre that an anchored scale makes is
|
|
474
|
+
the joint's to give -- the face on the joint stays there by itself (user, 2026-09-25: 「'R'모드에서 크기가 변하지 않는다」;
|
|
475
|
+
the shift had been taken for a move of the joint, and the size was dropped).
|
|
476
|
+
*/
|
|
477
|
+
const jointed = !!asset.joints?.some(j => j.body1.part === ownerOf(id));
|
|
478
|
+
const changes = gizmoChanges(id, this.reachOf(id), jointed && result.scale ? { scale: result.scale } : result, sizes);
|
|
479
|
+
if (changes.length) {
|
|
421
480
|
this.run(() => this.session.apply(changes));
|
|
422
|
-
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (jointed && result.scale) {
|
|
484
|
+
/* Only measures that follow another were pulled: the relation says their size, and it is not broken here. */
|
|
485
|
+
const axes = ['x', 'y', 'z'].filter((_, i) => Math.abs(result.scale[i] - 1) > 1e-4);
|
|
486
|
+
this.error = i18next.t('figure.text.gizmo-size-follows', { part: partLabelOf(id), axis: axes.join(', ').toUpperCase() });
|
|
487
|
+
return this.requestUpdate();
|
|
488
|
+
}
|
|
489
|
+
/*
|
|
490
|
+
A part whose place is worked out (a converted one) has no numbers for the gizmo to write: the drag is added beside
|
|
491
|
+
its rule (`nudge-part`). A part a joint or a fastening places, or a size that is worked out, is refused -- said in
|
|
492
|
+
words, not by the part silently springing back (user, 2026-09-24: 「기즈모 컨트롤도 전혀 작동이 안되는군」).
|
|
493
|
+
*/
|
|
494
|
+
const body = view.geometry.find(g => g.id === id);
|
|
495
|
+
if (!body)
|
|
496
|
+
return this.requestUpdate();
|
|
497
|
+
const { by, turn } = nudgeOf(body, result);
|
|
498
|
+
/* A size worked out from the figure's: the drag's factor beside its rule, so it still follows (V2 scaled any part). */
|
|
499
|
+
const grow = {};
|
|
500
|
+
if (result.scale)
|
|
501
|
+
['x', 'y', 'z'].forEach((axis, i) => Math.abs(result.scale[i] - 1) > 1e-6 && (grow[axis] = Math.round(result.scale[i] * 1e6) / 1e6));
|
|
502
|
+
if (!Object.keys(by).length && !Object.keys(turn).length && !Object.keys(grow).length)
|
|
503
|
+
return this.requestUpdate();
|
|
504
|
+
try {
|
|
505
|
+
this.session.command({
|
|
506
|
+
kind: 'nudge-part',
|
|
507
|
+
part: id,
|
|
508
|
+
...(Object.keys(by).length ? { by } : {}),
|
|
509
|
+
...(Object.keys(turn).length ? { turn } : {}),
|
|
510
|
+
...(Object.keys(grow).length ? { grow } : {})
|
|
511
|
+
});
|
|
512
|
+
this.error = '';
|
|
513
|
+
this.changed();
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
this.error = /carried by a joint/.test(message)
|
|
518
|
+
? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
|
|
519
|
+
: /is fastened to/.test(message)
|
|
520
|
+
? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
|
|
521
|
+
: message;
|
|
423
522
|
this.requestUpdate();
|
|
523
|
+
}
|
|
424
524
|
}
|
|
425
525
|
/** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
|
|
426
526
|
align(tool) {
|
|
@@ -486,7 +586,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
486
586
|
*/
|
|
487
587
|
renderRepeatNote(geometry) {
|
|
488
588
|
const repeat = repeatOf(this.selected, geometry);
|
|
489
|
-
return repeat ? html `<p hint>${i18next.t('figure.text.repeat-member', { name: repeat.name, count: repeat.count, index: repeat.index })}</p>` : nothing;
|
|
589
|
+
return repeat?.index ? html `<p hint>${i18next.t('figure.text.repeat-member', { name: repeat.name, count: repeat.count, index: repeat.index })}</p>` : nothing;
|
|
490
590
|
}
|
|
491
591
|
renamePart(input) {
|
|
492
592
|
const name = input.value.trim();
|
|
@@ -579,9 +679,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
579
679
|
? i18next.t('figure.text.follow-would-loop')
|
|
580
680
|
: code === 'FASTEN_CYCLE' || code === 'ATTACH_CYCLE'
|
|
581
681
|
? i18next.t('figure.text.fasten-would-loop')
|
|
582
|
-
: error instanceof Error
|
|
583
|
-
? error.message
|
|
584
|
-
: String(error);
|
|
682
|
+
: sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? (error instanceof Error ? error.message : String(error));
|
|
585
683
|
}
|
|
586
684
|
}
|
|
587
685
|
changeField(field, event) {
|
|
@@ -691,9 +789,118 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
691
789
|
<md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
|
|
692
790
|
</button>`;
|
|
693
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* The parts list, one row per part the author made: a repeat is one row with how many copies it makes now (V2 listed
|
|
794
|
+
* its parts the same way; user, 2026-09-24: the conveyor's sixteen rollers were sixteen rows).
|
|
795
|
+
*/
|
|
796
|
+
partRows(geometry) {
|
|
797
|
+
const rows = new Map();
|
|
798
|
+
for (const g of geometry) {
|
|
799
|
+
const id = ownerOf(g.id);
|
|
800
|
+
const row = rows.get(id);
|
|
801
|
+
if (row) {
|
|
802
|
+
row.copies = (row.copies ?? 1) + 1;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
rows.set(id, {
|
|
806
|
+
id,
|
|
807
|
+
provider: g.shape?.provider ?? '',
|
|
808
|
+
hidden: this.hiddenParts.has(id),
|
|
809
|
+
invisible: g.visible === false,
|
|
810
|
+
...(id !== g.id ? { copies: 1 } : {})
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return [...rows.values()];
|
|
814
|
+
}
|
|
815
|
+
/** The detail level's part budget for the list's head, from figure-model's count (a repeat once). */
|
|
816
|
+
partBudgetOf(asset) {
|
|
817
|
+
if (!asset)
|
|
818
|
+
return undefined;
|
|
819
|
+
try {
|
|
820
|
+
return { level: asset.detailLevel ?? null, used: v3PartCountOf(asset), limit: asset.detailLevel ? PART_LIMIT[asset.detailLevel] : null };
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* A repeat: which way its copies run, how many there are now, and how far apart -- V2's `repeat: { axis, pitch }`.
|
|
828
|
+
* The pitch of a row that fills its length is edited as a number (`set-repeat` makes it the part's own the first
|
|
829
|
+
* time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
|
|
830
|
+
*/
|
|
831
|
+
renderRepeat(asset, geometry, unavailable) {
|
|
832
|
+
const repeat = repeatOf(this.selected, geometry);
|
|
833
|
+
if (!asset || !repeat || repeat.index)
|
|
834
|
+
return nothing;
|
|
835
|
+
const model = asset.document.model;
|
|
836
|
+
const layout = model.nodes.find(n => n.id === `${this.selected}.layout`);
|
|
837
|
+
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
|
|
838
|
+
const own = `${this.selected}.repeat.pitch`;
|
|
839
|
+
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value;
|
|
840
|
+
const fits = layout?.op === 'fit-pitch@1';
|
|
841
|
+
/* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
|
|
842
|
+
const lengthRef = layout?.args[0] ?? '';
|
|
843
|
+
const length = asset.designInputs[lengthRef];
|
|
844
|
+
const longest = model.inputs.find(i => i.id === lengthRef)?.max;
|
|
845
|
+
const limit = model.constants.find(c => c.id === layout?.args[6])?.value;
|
|
846
|
+
const field = { id: own, unit: 'mm', role: 'design', value: pitch ?? 0 };
|
|
847
|
+
return html `<section>
|
|
848
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
849
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-axis')}</label><span value>${axis.toUpperCase()}</span></div>
|
|
850
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-count')}</label><span value>${repeat.count}</span></div>
|
|
851
|
+
<div inline-field>
|
|
852
|
+
<label>${i18next.t('figure.label.repeat-gap')} <span unit>mm</span></label>
|
|
853
|
+
<input
|
|
854
|
+
type="number"
|
|
855
|
+
step="any"
|
|
856
|
+
aria-label=${own}
|
|
857
|
+
.value=${String(pitch ?? '')}
|
|
858
|
+
?disabled=${unavailable || !fits}
|
|
859
|
+
@change=${(e) => this.setPitch(field, e)}
|
|
860
|
+
/>
|
|
861
|
+
</div>
|
|
862
|
+
<p hint>
|
|
863
|
+
${fits && length !== undefined && longest !== undefined && limit !== undefined
|
|
864
|
+
? i18next.t('figure.text.repeat-what-pitch-gives', {
|
|
865
|
+
length: length.toLocaleString(),
|
|
866
|
+
count: repeat.count,
|
|
867
|
+
longest: longest.toLocaleString(),
|
|
868
|
+
limit
|
|
869
|
+
})
|
|
870
|
+
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
871
|
+
</p>
|
|
872
|
+
</section>`;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
876
|
+
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
877
|
+
* that -- said with the numbers.
|
|
878
|
+
*/
|
|
879
|
+
setPitch(field, event) {
|
|
880
|
+
const input = event.target;
|
|
881
|
+
const pitch = input.valueAsNumber;
|
|
882
|
+
if (!Number.isFinite(pitch) || pitch === field.value)
|
|
883
|
+
return;
|
|
884
|
+
try {
|
|
885
|
+
this.session.command({ kind: 'set-repeat', part: this.selected, pitch });
|
|
886
|
+
this.error = '';
|
|
887
|
+
this.changed();
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
input.value = String(field.value);
|
|
891
|
+
const code = error?.code;
|
|
892
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
893
|
+
const found = /\((\d[\d.]*) mm\) a pitch under ([\d.]+) mm makes more than (\d+)/.exec(message);
|
|
894
|
+
this.error =
|
|
895
|
+
code === 'INPUT_RANGE' && found
|
|
896
|
+
? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
|
|
897
|
+
: message;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
694
900
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
695
901
|
choose(id) {
|
|
696
|
-
|
|
902
|
+
/* A copy on the canvas selects its repeat: the part the author made. */
|
|
903
|
+
this.selected = id ? ownerOf(id) : id;
|
|
697
904
|
if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
|
|
698
905
|
this.tab = 'properties';
|
|
699
906
|
}
|
|
@@ -731,11 +938,12 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
731
938
|
if (!view)
|
|
732
939
|
return html `<p>${i18next.t('figure.text.opening-model')}</p>`;
|
|
733
940
|
const unavailable = view.loading || !!view.error || view.saving || !view.context;
|
|
734
|
-
const part = view.geometry.find(g => g.id === this.selected);
|
|
941
|
+
const part = view.geometry.find(g => ownerOf(g.id) === this.selected);
|
|
735
942
|
/* The controls (state inputs) have their own place in the motion tab; these are the design values. */
|
|
736
943
|
const design = view.fields.filter(i => i.role !== 'state');
|
|
737
|
-
|
|
738
|
-
const
|
|
944
|
+
/* A repeat's own row values (its pitch) have their own section, not the shape's. */
|
|
945
|
+
const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.') && !i.id.startsWith(this.selected + '.repeat.'));
|
|
946
|
+
const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(ownerOf(g.id) + '.')));
|
|
739
947
|
const status = view.loading
|
|
740
948
|
? 'figure.text.loading-figure'
|
|
741
949
|
: view.saving
|
|
@@ -769,18 +977,15 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
769
977
|
slot="start"
|
|
770
978
|
.budget=${this.budget}
|
|
771
979
|
@show-maturity=${() => (this.tab = 'verdict')}
|
|
772
|
-
.parts=${view.geometry
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
hidden: this.hiddenParts.has(g.id),
|
|
776
|
-
invisible: g.visible === false
|
|
777
|
-
}))}
|
|
980
|
+
.parts=${this.partRows(view.geometry)}
|
|
981
|
+
.partBudget=${this.partBudgetOf(asset)}
|
|
982
|
+
@set-detail-level=${(e) => this.linkCommand({ kind: 'set-detail-level', level: e.detail.level })}
|
|
778
983
|
.selected=${this.selected}
|
|
779
984
|
?locked=${unavailable}
|
|
780
985
|
@add-part=${(e) => this.add(e.detail.kind)}
|
|
781
986
|
@select-part=${(e) => this.choose(e.detail.id)}
|
|
782
987
|
@toggle-part-hidden=${(e) => this.toggleHiddenPart(e.detail.id)}
|
|
783
|
-
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => g.id)) : new Set())}
|
|
988
|
+
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => ownerOf(g.id))) : new Set())}
|
|
784
989
|
@move-part-up=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
785
990
|
@move-part-down=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
786
991
|
@duplicate-part=${(e) => this.duplicatePart(e.detail.id)}
|
|
@@ -834,10 +1039,11 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
834
1039
|
</div>
|
|
835
1040
|
${this.renderRepeatNote(view.geometry)}
|
|
836
1041
|
</section>
|
|
837
|
-
${this.
|
|
1042
|
+
${this.renderRepeat(asset, view.geometry, unavailable)}
|
|
1043
|
+
${this.renderGroups(groupFields(own, this.selected), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable))}
|
|
838
1044
|
${this.renderLinks(unavailable, 'shape')}
|
|
839
1045
|
<figure-v3-material
|
|
840
|
-
.body=${part}
|
|
1046
|
+
.body=${{ ...part, id: this.selected }}
|
|
841
1047
|
.palette=${view.palette ?? {}}
|
|
842
1048
|
?locked=${unavailable}
|
|
843
1049
|
@v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
|
|
@@ -939,6 +1145,10 @@ __decorate([
|
|
|
939
1145
|
property({ attribute: false }),
|
|
940
1146
|
__metadata("design:type", Object)
|
|
941
1147
|
], FigureV3Editor.prototype, "budget", void 0);
|
|
1148
|
+
__decorate([
|
|
1149
|
+
property({ attribute: false }),
|
|
1150
|
+
__metadata("design:type", Function)
|
|
1151
|
+
], FigureV3Editor.prototype, "firstSave", void 0);
|
|
942
1152
|
__decorate([
|
|
943
1153
|
state(),
|
|
944
1154
|
__metadata("design:type", Object)
|