@things-factory/figure-ui 10.1.57 → 10.1.59
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/v3-editor.ts +32 -4
- package/client/modeller/v3-links.ts +39 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.js +31 -5
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +17 -1
- package/dist-client/modeller/v3-links.js +41 -1
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/browser/modeller.check.mjs +69 -0
- package/test/v3-links.test.ts +40 -1
- package/translations/en.json +4 -1
- package/translations/ja.json +4 -1
- package/translations/ko.json +4 -1
- package/translations/ms.json +4 -1
- package/translations/zh.json +4 -1
|
@@ -3,6 +3,7 @@ import { i18next } from '@operato/i18n'
|
|
|
3
3
|
import type { OxThreePane } from '@operato/layout'
|
|
4
4
|
import '@operato/layout'
|
|
5
5
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
6
|
+
import { live } from 'lit/directives/live.js'
|
|
6
7
|
import { PART_LIMIT, v3PartCountOf } from '@hatiolab/figure-model'
|
|
7
8
|
import type { V3Asset, V3AuthoringAction, V3InputChange, V3PartEdit, V3PrimitiveCreation } from '@hatiolab/figure-model'
|
|
8
9
|
import type { V3ProductSession } from './v3-product-session.js'
|
|
@@ -31,7 +32,7 @@ import { nudgeOf } from './v3-stage-model.js'
|
|
|
31
32
|
import { narrowingRetryOf, partRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js'
|
|
32
33
|
import type { V3RenderPart } from './v3-renderer.js'
|
|
33
34
|
import type { V3BudgetLine } from './v3-maturity.js'
|
|
34
|
-
import { instanceRangeOf, instanceRangeToDeclare, rangeDiffers, turnsAboutAJoint } from './v3-links.js'
|
|
35
|
+
import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js'
|
|
35
36
|
import { jointFanOf } from './v3-joint-fan.js'
|
|
36
37
|
import { jointDragOf } from './v3-joint-drag.js'
|
|
37
38
|
import type { FirstSave } from './v3-first-save.js'
|
|
@@ -764,7 +765,22 @@ export class FigureV3Editor extends LitElement {
|
|
|
764
765
|
<h3>${i18next.t('figure.label.instance-range')}</h3>
|
|
765
766
|
${(['x', 'y', 'z'] as const).map(
|
|
766
767
|
axis => html`<div inline-field>
|
|
767
|
-
<label>${axis.toUpperCase()}
|
|
768
|
+
<label>${axis.toUpperCase()} <span unit>mm</span></label>
|
|
769
|
+
<div pair>
|
|
770
|
+
${(['min', 'max'] as const).map(
|
|
771
|
+
bound => html`<input
|
|
772
|
+
aria-label=${`size.${axis}.${bound}`}
|
|
773
|
+
type="number"
|
|
774
|
+
step="any"
|
|
775
|
+
.value=${live(String(Math.round(range.declared[axis][bound])))}
|
|
776
|
+
?disabled=${unavailable}
|
|
777
|
+
@change=${(e: Event) => {
|
|
778
|
+
const next = rangeWith(range.declared, axis, bound, (e.target as HTMLInputElement).valueAsNumber)
|
|
779
|
+
if (next) this.linkCommand({ kind: 'declare-instance-size', range: next, replace: true })
|
|
780
|
+
}}
|
|
781
|
+
/>`
|
|
782
|
+
)}
|
|
783
|
+
</div>
|
|
768
784
|
</div>`
|
|
769
785
|
)}
|
|
770
786
|
${differs
|
|
@@ -788,7 +804,18 @@ export class FigureV3Editor extends LitElement {
|
|
|
788
804
|
* it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
|
|
789
805
|
*/
|
|
790
806
|
private renderSizingRule(asset: V3Asset, unavailable: boolean) {
|
|
791
|
-
if (asset.sizing === 'uniform')
|
|
807
|
+
if (asset.sizing === 'uniform') {
|
|
808
|
+
/* One ratio reaches only what reads the size: every other length is offered to follow it, in one step. */
|
|
809
|
+
const follow = proportionToFollow(asset)
|
|
810
|
+
return html`<div inline-field><label>${i18next.t('figure.label.sizing-rule')}</label><span>${i18next.t('figure.label.sizing-uniform')}</span></div>
|
|
811
|
+
${follow?.count
|
|
812
|
+
? html`<p hint>${i18next.t('figure.text.proportion-to-follow', { count: follow.count })}</p>
|
|
813
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'follow-size-in-proportion' })}>
|
|
814
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size-in-proportion')}
|
|
815
|
+
</button>`
|
|
816
|
+
: nothing}
|
|
817
|
+
${follow?.kept.length ? html`<p hint>${i18next.t('figure.text.proportion-kept', { what: follow.kept.join(', ') })}</p>` : nothing}`
|
|
818
|
+
}
|
|
792
819
|
return html`${turnsAboutAJoint(asset) ? html`<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}</p>` : nothing}
|
|
793
820
|
<button act ?disabled=${unavailable} title=${i18next.t('figure.text.sizing-uniform-explained')} @click=${() => this.linkCommand({ kind: 'scale-uniformly' })}>
|
|
794
821
|
<md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
|
|
@@ -842,7 +869,8 @@ export class FigureV3Editor extends LitElement {
|
|
|
842
869
|
const layout = model.nodes.find(n => n.id === `${this.selected}.layout`)
|
|
843
870
|
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?'
|
|
844
871
|
const own = `${this.selected}.repeat.pitch`
|
|
845
|
-
|
|
872
|
+
/* The pitch as it stands: the part's own number, a constant, or what a pitch that follows a length comes to. */
|
|
873
|
+
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(asset, layout?.args[4])
|
|
846
874
|
const fits = layout?.op === 'fit-pitch@1'
|
|
847
875
|
/* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
|
|
848
876
|
const lengthRef = layout?.args[0] ?? ''
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V3_FACES, v3ShapeFaces, v3PartsExtentOf, compileV3Graph, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model'
|
|
1
|
+
import { V3_FACES, v3ShapeFaces, v3PartsExtentOf, compileV3Graph, proposeV3Proportion, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { V3SizeRange } from '@hatiolab/figure-model'
|
|
3
3
|
import type { V3Asset, V3BindingPlace, V3Face, V3Joint } from '@hatiolab/figure-model'
|
|
4
4
|
|
|
@@ -442,6 +442,44 @@ export function instanceRangeToDeclare(asset: V3Asset): { range: V3SizeRange; at
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
/** Whether measuring again would change the declared range (to a micrometre). */
|
|
445
|
+
/**
|
|
446
|
+
* The declared range with one end typed by the author: the sizes a figure may be placed at are a claim about the
|
|
447
|
+
* product (a gantry spans metres, not 30 cm to 29 m), and measuring only ever widens them. Nothing when the typed
|
|
448
|
+
* value is not a number; whether the figure holds there is the command's to say.
|
|
449
|
+
*/
|
|
450
|
+
export function rangeWith(declared: V3SizeRange, axis: Axis, bound: 'min' | 'max', value: number): V3SizeRange | undefined {
|
|
451
|
+
if (!Number.isFinite(value)) return undefined
|
|
452
|
+
const next = structuredClone(declared)
|
|
453
|
+
next[axis][bound] = value
|
|
454
|
+
return next
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* For a figure that grows by one ratio: how many of its lengths would follow the ratio, and which it leaves with their
|
|
459
|
+
* own number (not those that follow another length -- they grow with it). Nothing for a figure of another sizing.
|
|
460
|
+
*/
|
|
461
|
+
export function proportionToFollow(asset: V3Asset): { count: number; kept: string[] } | undefined {
|
|
462
|
+
if (asset.sizing !== 'uniform') return undefined
|
|
463
|
+
try {
|
|
464
|
+
const plan = proposeV3Proportion(asset)
|
|
465
|
+
return { count: plan.dimensions.length + plan.lengths.length, kept: plan.skipped.filter(s => !s.follows).map(s => s.what) }
|
|
466
|
+
} catch {
|
|
467
|
+
return undefined
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** A value of the figure as it stands, by its graph reference. */
|
|
472
|
+
export function valueNow(asset: V3Asset, ref: string | undefined): number | undefined {
|
|
473
|
+
if (!ref) return undefined
|
|
474
|
+
try {
|
|
475
|
+
const values = compileV3Graph(asset.document.model as never).evaluate({ ...asset.designInputs, ...asset.stateDefaults }).values as Record<string, number>
|
|
476
|
+
const value = values[ref]
|
|
477
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
478
|
+
} catch {
|
|
479
|
+
return undefined
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
445
483
|
export function rangeDiffers(a: V3SizeRange, b: V3SizeRange): boolean {
|
|
446
484
|
return AXES.some(axis => Math.abs(a[axis].min - b[axis].min) > 1e-3 || Math.abs(a[axis].max - b[axis].max) > 1e-3)
|
|
447
485
|
}
|
|
@@ -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 { live } from 'lit/directives/live.js';
|
|
7
8
|
import { PART_LIMIT, v3PartCountOf } from '@hatiolab/figure-model';
|
|
8
9
|
import { captureV3Thumbnail } from './v3-thumbnail.js';
|
|
9
10
|
import { drawableV3Parts } from './v3-renderer.js';
|
|
@@ -26,7 +27,7 @@ import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
|
|
|
26
27
|
import { paletteRecipe } from './v3-palette.js';
|
|
27
28
|
import { nudgeOf } from './v3-stage-model.js';
|
|
28
29
|
import { narrowingRetryOf, partRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js';
|
|
29
|
-
import { instanceRangeOf, instanceRangeToDeclare, rangeDiffers, turnsAboutAJoint } from './v3-links.js';
|
|
30
|
+
import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js';
|
|
30
31
|
import { jointFanOf } from './v3-joint-fan.js';
|
|
31
32
|
import { jointDragOf } from './v3-joint-drag.js';
|
|
32
33
|
import { liveOverSession, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js';
|
|
@@ -778,7 +779,21 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
778
779
|
return html `<section>
|
|
779
780
|
<h3>${i18next.t('figure.label.instance-range')}</h3>
|
|
780
781
|
${['x', 'y', 'z'].map(axis => html `<div inline-field>
|
|
781
|
-
<label>${axis.toUpperCase()}
|
|
782
|
+
<label>${axis.toUpperCase()} <span unit>mm</span></label>
|
|
783
|
+
<div pair>
|
|
784
|
+
${['min', 'max'].map(bound => html `<input
|
|
785
|
+
aria-label=${`size.${axis}.${bound}`}
|
|
786
|
+
type="number"
|
|
787
|
+
step="any"
|
|
788
|
+
.value=${live(String(Math.round(range.declared[axis][bound])))}
|
|
789
|
+
?disabled=${unavailable}
|
|
790
|
+
@change=${(e) => {
|
|
791
|
+
const next = rangeWith(range.declared, axis, bound, e.target.valueAsNumber);
|
|
792
|
+
if (next)
|
|
793
|
+
this.linkCommand({ kind: 'declare-instance-size', range: next, replace: true });
|
|
794
|
+
}}
|
|
795
|
+
/>`)}
|
|
796
|
+
</div>
|
|
782
797
|
</div>`)}
|
|
783
798
|
${differs
|
|
784
799
|
? html `<p hint>
|
|
@@ -801,8 +816,18 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
801
816
|
* it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
|
|
802
817
|
*/
|
|
803
818
|
renderSizingRule(asset, unavailable) {
|
|
804
|
-
if (asset.sizing === 'uniform')
|
|
805
|
-
|
|
819
|
+
if (asset.sizing === 'uniform') {
|
|
820
|
+
/* One ratio reaches only what reads the size: every other length is offered to follow it, in one step. */
|
|
821
|
+
const follow = proportionToFollow(asset);
|
|
822
|
+
return html `<div inline-field><label>${i18next.t('figure.label.sizing-rule')}</label><span>${i18next.t('figure.label.sizing-uniform')}</span></div>
|
|
823
|
+
${follow?.count
|
|
824
|
+
? html `<p hint>${i18next.t('figure.text.proportion-to-follow', { count: follow.count })}</p>
|
|
825
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'follow-size-in-proportion' })}>
|
|
826
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size-in-proportion')}
|
|
827
|
+
</button>`
|
|
828
|
+
: nothing}
|
|
829
|
+
${follow?.kept.length ? html `<p hint>${i18next.t('figure.text.proportion-kept', { what: follow.kept.join(', ') })}</p>` : nothing}`;
|
|
830
|
+
}
|
|
806
831
|
return html `${turnsAboutAJoint(asset) ? html `<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}</p>` : nothing}
|
|
807
832
|
<button act ?disabled=${unavailable} title=${i18next.t('figure.text.sizing-uniform-explained')} @click=${() => this.linkCommand({ kind: 'scale-uniformly' })}>
|
|
808
833
|
<md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
|
|
@@ -855,7 +880,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
855
880
|
const layout = model.nodes.find(n => n.id === `${this.selected}.layout`);
|
|
856
881
|
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
|
|
857
882
|
const own = `${this.selected}.repeat.pitch`;
|
|
858
|
-
|
|
883
|
+
/* The pitch as it stands: the part's own number, a constant, or what a pitch that follows a length comes to. */
|
|
884
|
+
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(asset, layout?.args[4]);
|
|
859
885
|
const fits = layout?.op === 'fit-pitch@1';
|
|
860
886
|
/* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
|
|
861
887
|
const lengthRef = layout?.args[0] ?? '';
|