@things-factory/figure-ui 10.1.58 → 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.
@@ -32,7 +32,7 @@ import { nudgeOf } from './v3-stage-model.js'
32
32
  import { narrowingRetryOf, partRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js'
33
33
  import type { V3RenderPart } from './v3-renderer.js'
34
34
  import type { V3BudgetLine } from './v3-maturity.js'
35
- import { instanceRangeOf, instanceRangeToDeclare, rangeDiffers, rangeWith, turnsAboutAJoint } from './v3-links.js'
35
+ import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js'
36
36
  import { jointFanOf } from './v3-joint-fan.js'
37
37
  import { jointDragOf } from './v3-joint-drag.js'
38
38
  import type { FirstSave } from './v3-first-save.js'
@@ -804,7 +804,18 @@ export class FigureV3Editor extends LitElement {
804
804
  * it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
805
805
  */
806
806
  private renderSizingRule(asset: V3Asset, unavailable: boolean) {
807
- if (asset.sizing === 'uniform') return html`<div inline-field><label>${i18next.t('figure.label.sizing-rule')}</label><span>${i18next.t('figure.label.sizing-uniform')}</span></div>`
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
+ }
808
819
  return html`${turnsAboutAJoint(asset) ? html`<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}</p>` : nothing}
809
820
  <button act ?disabled=${unavailable} title=${i18next.t('figure.text.sizing-uniform-explained')} @click=${() => this.linkCommand({ kind: 'scale-uniformly' })}>
810
821
  <md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
@@ -858,7 +869,8 @@ export class FigureV3Editor extends LitElement {
858
869
  const layout = model.nodes.find(n => n.id === `${this.selected}.layout`)
859
870
  const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?'
860
871
  const own = `${this.selected}.repeat.pitch`
861
- const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value
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])
862
874
  const fits = layout?.op === 'fit-pitch@1'
863
875
  /* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
864
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
 
@@ -454,6 +454,32 @@ export function rangeWith(declared: V3SizeRange, axis: Axis, bound: 'min' | 'max
454
454
  return next
455
455
  }
456
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
+
457
483
  export function rangeDiffers(a: V3SizeRange, b: V3SizeRange): boolean {
458
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)
459
485
  }
@@ -34,6 +34,6 @@ export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<P
34
34
  state: string;
35
35
  clip: string;
36
36
  keys: ClipKey[];
37
- loop: "wrap" | "hold";
37
+ loop: "hold" | "wrap";
38
38
  interpolation: "linear" | "step";
39
39
  };
@@ -27,7 +27,7 @@ import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
27
27
  import { paletteRecipe } from './v3-palette.js';
28
28
  import { nudgeOf } from './v3-stage-model.js';
29
29
  import { narrowingRetryOf, partRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js';
30
- import { instanceRangeOf, instanceRangeToDeclare, rangeDiffers, rangeWith, turnsAboutAJoint } from './v3-links.js';
30
+ import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js';
31
31
  import { jointFanOf } from './v3-joint-fan.js';
32
32
  import { jointDragOf } from './v3-joint-drag.js';
33
33
  import { liveOverSession, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js';
@@ -816,8 +816,18 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
816
816
  * it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
817
817
  */
818
818
  renderSizingRule(asset, unavailable) {
819
- if (asset.sizing === 'uniform')
820
- return html `<div inline-field><label>${i18next.t('figure.label.sizing-rule')}</label><span>${i18next.t('figure.label.sizing-uniform')}</span></div>`;
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
+ }
821
831
  return html `${turnsAboutAJoint(asset) ? html `<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}</p>` : nothing}
822
832
  <button act ?disabled=${unavailable} title=${i18next.t('figure.text.sizing-uniform-explained')} @click=${() => this.linkCommand({ kind: 'scale-uniformly' })}>
823
833
  <md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
@@ -870,7 +880,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
870
880
  const layout = model.nodes.find(n => n.id === `${this.selected}.layout`);
871
881
  const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
872
882
  const own = `${this.selected}.repeat.pitch`;
873
- const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value;
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]);
874
885
  const fits = layout?.op === 'fit-pitch@1';
875
886
  /* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
876
887
  const lengthRef = layout?.args[0] ?? '';