@things-factory/figure-ui 10.1.74 → 10.1.75

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.
@@ -31,7 +31,7 @@ import type { V3PaletteKind, V3PartRow } from './figure-v3-parts.js'
31
31
  import { paletteRecipe } from './v3-palette.js'
32
32
  import type { FigureV3Canvas } from './v3-canvas.js'
33
33
  import { nudgeOf } from './v3-stage-model.js'
34
- import { inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js'
34
+ import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './v3-refusals.js'
35
35
  import type { V3RenderPart } from './v3-renderer.js'
36
36
  import type { V3BudgetLine } from './v3-maturity.js'
37
37
  import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js'
@@ -402,14 +402,23 @@ export class FigureV3Editor extends LitElement {
402
402
  this.renderRoot.querySelector<FigureV3Canvas>('figure-v3-canvas')?.gizmo(mode)
403
403
  this.gizmoMode = this.gizmoMode === mode ? undefined : mode
404
404
  }
405
+ /**
406
+ * A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
407
+ * (the network, the server) as it came.
408
+ */
409
+ private said(error: unknown): string {
410
+ return (error as { code?: string })?.code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error)
411
+ }
405
412
  private run(action: () => unknown) {
406
413
  try {
407
414
  action()
408
415
  this.error = ''
416
+ /* The next step stands on its own: what the last removal let go of is no longer the news. */
417
+ this.notice = ''
409
418
  this.changed()
410
419
  } catch (error) {
411
420
  const say = (key: string, params: Record<string, string>) => i18next.t(key, params)
412
- this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? (error instanceof Error ? error.message : String(error))
421
+ this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? this.said(error)
413
422
  } finally {
414
423
  /* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
415
424
  this.live = NOTHING_LIVE
@@ -475,7 +484,7 @@ export class FigureV3Editor extends LitElement {
475
484
  await promise
476
485
  this.error = ''
477
486
  } catch (error) {
478
- this.error = error instanceof Error ? error.message : String(error)
487
+ this.error = this.said(error)
479
488
  } finally {
480
489
  this.changed()
481
490
  }
@@ -508,13 +517,15 @@ export class FigureV3Editor extends LitElement {
508
517
  private removePart(id: string, refitRoom = false) {
509
518
  this.retry = undefined
510
519
  try {
511
- this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) })
520
+ const view = this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
512
521
  this.error = ''
522
+ /* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
523
+ this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? ''
513
524
  if (this.selected === id) this.selected = ''
514
525
  this.changed()
515
526
  } catch (error) {
516
527
  /* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
517
- this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? (error instanceof Error ? error.message : String(error))
528
+ this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error)
518
529
  /* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
519
530
  if (!refitRoom && inDeclaredRoom(error)) this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) }
520
531
  }
@@ -546,7 +557,7 @@ export class FigureV3Editor extends LitElement {
546
557
  this.error = ''
547
558
  this.changed()
548
559
  } catch (error) {
549
- this.recipeError = error instanceof Error ? error.message : String(error)
560
+ this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error)
550
561
  }
551
562
  }
552
563
  /** The design values of the asset being edited. */
@@ -630,7 +641,7 @@ export class FigureV3Editor extends LitElement {
630
641
  ? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
631
642
  : /is fastened to/.test(message)
632
643
  ? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
633
- : message
644
+ : this.said(error)
634
645
  this.requestUpdate()
635
646
  }
636
647
  }
@@ -803,7 +814,7 @@ export class FigureV3Editor extends LitElement {
803
814
  ? i18next.t('figure.text.fasten-would-loop')
804
815
  : sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
805
816
  placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
806
- (error instanceof Error ? error.message : String(error))
817
+ this.said(error)
807
818
  }
808
819
  }
809
820
  private changeField(field: { id: string; unit: V3InputChange['unit']; role?: string }, event: Event) {
@@ -1095,7 +1106,7 @@ export class FigureV3Editor extends LitElement {
1095
1106
  this.error = ''
1096
1107
  this.changed()
1097
1108
  } catch (error) {
1098
- this.error = error instanceof Error ? error.message : String(error)
1109
+ this.error = this.said(error)
1099
1110
  }
1100
1111
  }
1101
1112
  return html`<div public-pitch>
@@ -1152,7 +1163,7 @@ export class FigureV3Editor extends LitElement {
1152
1163
  this.error =
1153
1164
  code === 'INPUT_RANGE' && found
1154
1165
  ? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
1155
- : message
1166
+ : this.said(error)
1156
1167
  }
1157
1168
  }
1158
1169
  /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
@@ -39,11 +39,6 @@ export function narrowingRetryOf<A extends { kind: string }>(action: A, error: u
39
39
  }
40
40
  }
41
41
 
42
- /**
43
- * Why a part cannot be deleted, in the author's words -- figure-model names four reasons (PART_REFERENCED): parts hang
44
- * from it, the declared space holds it, another part follows its size or place, or a joint moves with its joint. One
45
- * sentence for all of them told the author to undo a link that was not there (the architect's workbench, 2026-09-25).
46
- */
47
42
  /**
48
43
  * A placement refused because a part is fastened to the mounting plane by a face the new plane does not take (a bottom
49
44
  * on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
@@ -61,20 +56,74 @@ export function inDeclaredRoom(error: unknown): boolean {
61
56
  return (error as { code?: string })?.code === 'PART_REFERENCED' && /is in the space the figure declares/.test(error instanceof Error ? error.message : String(error))
62
57
  }
63
58
 
59
+ /**
60
+ * Why a part cannot be deleted, in the author's words. Since the chief architect's ruling of 2026-09-27 a part that
61
+ * holds others, leads a motion or is followed goes, and what depended on it is let go of (`removalNoticeOf`); what is
62
+ * left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.
63
+ */
64
64
  export function partRefusalOf(error: unknown, part: string, say: Say): string | null {
65
- if ((error as { code?: string })?.code !== 'PART_REFERENCED') return null
66
- /* figure-model's sentence after its code and path: 「PART_REFERENCED: top: top holds leg-2, lamp; …」. */
65
+ const code = (error as { code?: string })?.code
66
+ if (code !== 'PART_REFERENCED' && code !== 'PART_ROW') return null
67
+ /* figure-model's sentence after its code and path: 「PART_ROW: bed: roller is a row laid along bed; …」. */
67
68
  const message = (error instanceof Error ? error.message : String(error)).replace(/^[A-Z_]+: [^:]+: /, '')
68
- const holds = / holds (.+?); take (?:them|it) off first/.exec(message)
69
- if (holds) return say('figure.text.part-holds-others', { part, others: holds[1]! })
70
69
  if (/is in the space the figure declares/.test(message)) return say('figure.text.part-in-declared-space', { part })
71
- const moves = /^(.+?) (?:moves|move) with /.exec(message)
72
- if (moves) return say('figure.text.part-is-led', { part, others: moves[1]! })
73
- const follows = /^(.+?) (?:follows|follow) .+'s size or place/.exec(message)
74
- if (follows) return say('figure.text.part-is-followed', { part, others: follows[1]! })
70
+ const along = /^(.+?) (?:is a row|are rows) laid along /.exec(message)
71
+ if (along) return say('figure.text.part-row-along', { part, rows: along[1]! })
72
+ const carried = /^(.+?) hangs? in the row /.exec(message)
73
+ if (carried) return say('figure.text.part-row-carries', { part, others: carried[1]! })
75
74
  return say('figure.text.part-is-referenced', { part })
76
75
  }
77
76
 
77
+ /** What removing a part let go of (figure-model's `RemovalNotice`), said in one line, or nothing when it let go of nothing. */
78
+ export function removalNoticeOf(
79
+ notice:
80
+ | {
81
+ released: Array<{ part: string; control?: string }>
82
+ fixed: Array<{ part: string }>
83
+ controls: Array<{ part: string; control: string; min: number; max: number }>
84
+ mountingFace?: boolean
85
+ bindings?: Array<{ id: string; label?: string }>
86
+ }
87
+ | undefined,
88
+ say: Say
89
+ ): string | null {
90
+ if (!notice) return null
91
+ const number = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 3 })
92
+ const lines: string[] = []
93
+ const still = notice.released.filter(r => !r.control).map(r => r.part)
94
+ if (still.length) lines.push(say('figure.text.removal-released', { parts: still.join(', ') }))
95
+ for (const r of notice.released.filter(r => r.control)) lines.push(say('figure.text.removal-released-motion', { part: r.part, control: r.control! }))
96
+ for (const c of notice.controls) lines.push(say('figure.text.removal-own-control', { part: c.part, control: c.control, min: number(c.min), max: number(c.max) }))
97
+ const fixed = [...new Set(notice.fixed.map(f => f.part))]
98
+ if (fixed.length) lines.push(say('figure.text.removal-fixed', { parts: fixed.join(', ') }))
99
+ if (notice.bindings?.length) lines.push(say('figure.text.removal-bindings', { places: notice.bindings.map(b => b.label ?? b.id).join(', ') }))
100
+ if (notice.mountingFace) lines.push(say('figure.text.removal-mounting-face', {}))
101
+ return lines.length ? lines.join(' ') : null
102
+ }
103
+
104
+ /**
105
+ * A recipe refused at one of its steps: which step, and why -- the particular sentence when there is one (a size
106
+ * range, a value's range, a placement), otherwise the kernel's code. The kernel says 「steps[2]: attach: …」.
107
+ */
108
+ export function recipeRefusalOf(error: unknown, say: Say): string | null {
109
+ const step = /^steps\[(\d+)\]$/.exec((error as { path?: string })?.path ?? '')
110
+ if (!step) return null
111
+ const why = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? placementRefusalOf(error, say)
112
+ return say('figure.text.recipe-step-refused', { step: String(Number(step[1]) + 1), why: why ?? '', code: (error as { code?: string })?.code ?? '' })
113
+ }
114
+
115
+ /**
116
+ * A refusal no sentence above knows, said as a refusal with its code -- never the kernel's English on the screen (the
117
+ * duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」, 2026-09-27). The figure's node limit
118
+ * is said with its number.
119
+ */
120
+ export function editRefusalOf(error: unknown, say: Say): string {
121
+ const code = (error as { code?: string })?.code
122
+ const message = error instanceof Error ? error.message : String(error)
123
+ if (code === 'DOCUMENT_LIMIT') return say('figure.text.edit-refused-node-limit', { max: /maximum (\d+) nodes/.exec(message)?.[1] ?? '' })
124
+ return say('figure.text.edit-refused', { code: code ?? '' })
125
+ }
126
+
78
127
  /**
79
128
  * A value outside the range its input declares, with the range and the value given -- the kernel says
80
129
  * 「size.x: 6021100 outside [300,1200]」. The figure's own size is named by its axis; any other input by its range only.
@@ -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: "hold" | "wrap";
37
+ loop: "wrap" | "hold";
38
38
  interpolation: "linear" | "step";
39
39
  };
@@ -79,6 +79,11 @@ export declare class FigureV3Editor extends LitElement {
79
79
  protected willUpdate(_changed: PropertyValues): void;
80
80
  /** The scene toggles: the mode already shown takes the gizmo away. */
81
81
  private useGizmo;
82
+ /**
83
+ * A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
84
+ * (the network, the server) as it came.
85
+ */
86
+ private said;
82
87
  private run;
83
88
  /** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
84
89
  private drawnParts;
@@ -27,7 +27,7 @@ import { ScrollbarStyles } from '@operato/styles';
27
27
  import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
28
28
  import { paletteRecipe } from './v3-palette.js';
29
29
  import { nudgeOf } from './v3-stage-model.js';
30
- import { inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js';
30
+ import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './v3-refusals.js';
31
31
  import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js';
32
32
  import { jointFanOf } from './v3-joint-fan.js';
33
33
  import { jointDragOf } from './v3-joint-drag.js';
@@ -376,15 +376,24 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
376
376
  this.renderRoot.querySelector('figure-v3-canvas')?.gizmo(mode);
377
377
  this.gizmoMode = this.gizmoMode === mode ? undefined : mode;
378
378
  }
379
+ /**
380
+ * A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
381
+ * (the network, the server) as it came.
382
+ */
383
+ said(error) {
384
+ return error?.code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error);
385
+ }
379
386
  run(action) {
380
387
  try {
381
388
  action();
382
389
  this.error = '';
390
+ /* The next step stands on its own: what the last removal let go of is no longer the news. */
391
+ this.notice = '';
383
392
  this.changed();
384
393
  }
385
394
  catch (error) {
386
395
  const say = (key, params) => i18next.t(key, params);
387
- this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? (error instanceof Error ? error.message : String(error));
396
+ this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? this.said(error);
388
397
  }
389
398
  finally {
390
399
  /* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
@@ -456,7 +465,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
456
465
  this.error = '';
457
466
  }
458
467
  catch (error) {
459
- this.error = error instanceof Error ? error.message : String(error);
468
+ this.error = this.said(error);
460
469
  }
461
470
  finally {
462
471
  this.changed();
@@ -492,15 +501,17 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
492
501
  removePart(id, refitRoom = false) {
493
502
  this.retry = undefined;
494
503
  try {
495
- this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
504
+ const view = this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
496
505
  this.error = '';
506
+ /* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
507
+ this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? '';
497
508
  if (this.selected === id)
498
509
  this.selected = '';
499
510
  this.changed();
500
511
  }
501
512
  catch (error) {
502
513
  /* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
503
- this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? (error instanceof Error ? error.message : String(error));
514
+ this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error);
504
515
  /* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
505
516
  if (!refitRoom && inDeclaredRoom(error))
506
517
  this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) };
@@ -536,7 +547,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
536
547
  this.changed();
537
548
  }
538
549
  catch (error) {
539
- this.recipeError = error instanceof Error ? error.message : String(error);
550
+ this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error);
540
551
  }
541
552
  }
542
553
  /** The design values of the asset being edited. */
@@ -629,7 +640,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
629
640
  ? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
630
641
  : /is fastened to/.test(message)
631
642
  ? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
632
- : message;
643
+ : this.said(error);
633
644
  this.requestUpdate();
634
645
  }
635
646
  }
@@ -810,7 +821,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
810
821
  ? i18next.t('figure.text.fasten-would-loop')
811
822
  : sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
812
823
  placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
813
- (error instanceof Error ? error.message : String(error));
824
+ this.said(error);
814
825
  }
815
826
  }
816
827
  changeField(field, event) {
@@ -1104,7 +1115,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
1104
1115
  this.changed();
1105
1116
  }
1106
1117
  catch (error) {
1107
- this.error = error instanceof Error ? error.message : String(error);
1118
+ this.error = this.said(error);
1108
1119
  }
1109
1120
  };
1110
1121
  return html `<div public-pitch>
@@ -1162,7 +1173,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
1162
1173
  this.error =
1163
1174
  code === 'INPUT_RANGE' && found
1164
1175
  ? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
1165
- : message;
1176
+ : this.said(error);
1166
1177
  }
1167
1178
  }
1168
1179
  /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */