@things-factory/figure-ui 10.1.48 → 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 +85 -10
- package/client/modeller/side-panel-styles.ts +8 -0
- package/client/modeller/v3-canvas.ts +25 -5
- package/client/modeller/v3-editor.ts +159 -22
- package/client/modeller/v3-fields.ts +15 -1
- package/client/modeller/v3-stage-model.ts +24 -0
- package/dist-client/modeller/figure-v3-parts.d.ts +13 -0
- package/dist-client/modeller/figure-v3-parts.js +80 -10
- package/dist-client/modeller/figure-v3-parts.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 +2 -0
- package/dist-client/modeller/v3-canvas.js +27 -5
- 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 +19 -0
- package/dist-client/modeller/v3-editor.js +160 -20
- 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-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/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/v3-fields.test.ts +37 -1
- package/test/v3-gizmo.test.ts +3 -1
- package/test/v3-maturity.test.ts +1 -1
- package/translations/en.json +11 -1
- package/translations/ja.json +11 -1
- package/translations/ko.json +11 -1
- package/translations/ms.json +11 -1
- package/translations/zh.json +11 -1
|
@@ -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,10 +19,11 @@ 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';
|
|
23
24
|
import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
|
|
24
25
|
import { paletteRecipe } from './v3-palette.js';
|
|
26
|
+
import { nudgeOf } from './v3-stage-model.js';
|
|
25
27
|
import { instanceRangeOf, rangeDiffers, turnsAboutAJoint } from './v3-links.js';
|
|
26
28
|
import { jointFanOf } from './v3-joint-fan.js';
|
|
27
29
|
import { isLive, livePartsOf, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js';
|
|
@@ -280,7 +282,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
280
282
|
this.error = '';
|
|
281
283
|
this.hiddenParts = new Set();
|
|
282
284
|
}
|
|
283
|
-
if (view && !view.geometry.some(g => g.id === this.selected))
|
|
285
|
+
if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected))
|
|
284
286
|
this.selected = '';
|
|
285
287
|
}
|
|
286
288
|
run(action) {
|
|
@@ -300,7 +302,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
300
302
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
301
303
|
drawnParts(geometry, asset) {
|
|
302
304
|
const parts = (isLive(this.live) && asset ? livePartsOf(asset, this.live) : null) ?? drawableV3Parts(geometry ?? []);
|
|
303
|
-
return parts.filter(body => !this.hiddenParts.has(body.id));
|
|
305
|
+
return parts.filter(body => !this.hiddenParts.has(ownerOf(body.id)));
|
|
304
306
|
}
|
|
305
307
|
/**
|
|
306
308
|
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
@@ -403,7 +405,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
403
405
|
/** What the gizmo may write for the selected part — read off the asset being edited. */
|
|
404
406
|
reachOf(id) {
|
|
405
407
|
const none = { translate: false, rotate: false, scale: false };
|
|
406
|
-
|
|
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))
|
|
407
410
|
return none;
|
|
408
411
|
try {
|
|
409
412
|
return gizmoReachOf(JSON.parse(this.session.source()), id);
|
|
@@ -417,10 +420,39 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
417
420
|
const view = this.session.read();
|
|
418
421
|
const sizes = Object.fromEntries(view.fields.map(field => [field.id, field.value]));
|
|
419
422
|
const changes = gizmoChanges(id, this.reachOf(id), result, sizes);
|
|
420
|
-
if (changes.length)
|
|
423
|
+
if (changes.length) {
|
|
421
424
|
this.run(() => this.session.apply(changes));
|
|
422
|
-
|
|
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;
|
|
423
454
|
this.requestUpdate();
|
|
455
|
+
}
|
|
424
456
|
}
|
|
425
457
|
/** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
|
|
426
458
|
align(tool) {
|
|
@@ -486,7 +518,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
486
518
|
*/
|
|
487
519
|
renderRepeatNote(geometry) {
|
|
488
520
|
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;
|
|
521
|
+
return repeat?.index ? html `<p hint>${i18next.t('figure.text.repeat-member', { name: repeat.name, count: repeat.count, index: repeat.index })}</p>` : nothing;
|
|
490
522
|
}
|
|
491
523
|
renamePart(input) {
|
|
492
524
|
const name = input.value.trim();
|
|
@@ -691,9 +723,118 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
691
723
|
<md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
|
|
692
724
|
</button>`;
|
|
693
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>
|
|
806
|
+
</section>`;
|
|
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
|
+
}
|
|
694
834
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
695
835
|
choose(id) {
|
|
696
|
-
|
|
836
|
+
/* A copy on the canvas selects its repeat: the part the author made. */
|
|
837
|
+
this.selected = id ? ownerOf(id) : id;
|
|
697
838
|
if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
|
|
698
839
|
this.tab = 'properties';
|
|
699
840
|
}
|
|
@@ -731,11 +872,12 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
731
872
|
if (!view)
|
|
732
873
|
return html `<p>${i18next.t('figure.text.opening-model')}</p>`;
|
|
733
874
|
const unavailable = view.loading || !!view.error || view.saving || !view.context;
|
|
734
|
-
const part = view.geometry.find(g => g.id === this.selected);
|
|
875
|
+
const part = view.geometry.find(g => ownerOf(g.id) === this.selected);
|
|
735
876
|
/* The controls (state inputs) have their own place in the motion tab; these are the design values. */
|
|
736
877
|
const design = view.fields.filter(i => i.role !== 'state');
|
|
737
|
-
|
|
738
|
-
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) + '.')));
|
|
739
881
|
const status = view.loading
|
|
740
882
|
? 'figure.text.loading-figure'
|
|
741
883
|
: view.saving
|
|
@@ -769,18 +911,15 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
769
911
|
slot="start"
|
|
770
912
|
.budget=${this.budget}
|
|
771
913
|
@show-maturity=${() => (this.tab = 'verdict')}
|
|
772
|
-
.parts=${view.geometry
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
hidden: this.hiddenParts.has(g.id),
|
|
776
|
-
invisible: g.visible === false
|
|
777
|
-
}))}
|
|
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 })}
|
|
778
917
|
.selected=${this.selected}
|
|
779
918
|
?locked=${unavailable}
|
|
780
919
|
@add-part=${(e) => this.add(e.detail.kind)}
|
|
781
920
|
@select-part=${(e) => this.choose(e.detail.id)}
|
|
782
921
|
@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())}
|
|
922
|
+
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => ownerOf(g.id))) : new Set())}
|
|
784
923
|
@move-part-up=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
785
924
|
@move-part-down=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
786
925
|
@duplicate-part=${(e) => this.duplicatePart(e.detail.id)}
|
|
@@ -834,10 +973,11 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
834
973
|
</div>
|
|
835
974
|
${this.renderRepeatNote(view.geometry)}
|
|
836
975
|
</section>
|
|
837
|
-
${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))}
|
|
838
978
|
${this.renderLinks(unavailable, 'shape')}
|
|
839
979
|
<figure-v3-material
|
|
840
|
-
.body=${part}
|
|
980
|
+
.body=${{ ...part, id: this.selected }}
|
|
841
981
|
.palette=${view.palette ?? {}}
|
|
842
982
|
?locked=${unavailable}
|
|
843
983
|
@v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
|