@things-factory/figure-ui 10.1.129 → 10.1.141

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.
Files changed (95) hide show
  1. package/client/modeller/align.ts +24 -18
  2. package/client/modeller/editor.ts +165 -253
  3. package/client/modeller/fasten-geometry.ts +12 -9
  4. package/client/modeller/figure-facts.ts +12 -14
  5. package/client/modeller/figure-links.ts +16 -12
  6. package/client/modeller/figure-preview.ts +13 -4
  7. package/client/modeller/figure-properties.ts +6 -5
  8. package/client/modeller/figure-verdict.ts +14 -8
  9. package/client/modeller/joint-fan.ts +15 -17
  10. package/client/modeller/links.ts +17 -23
  11. package/client/modeller/live.ts +6 -3
  12. package/client/modeller/motion-ends.ts +6 -7
  13. package/client/modeller/occupancy.ts +2 -3
  14. package/client/modeller/product-session.ts +15 -0
  15. package/client/modeller/revision.ts +66 -0
  16. package/client/pages/figure-modeller-page.ts +5 -18
  17. package/dist-client/modeller/align.d.ts +12 -7
  18. package/dist-client/modeller/align.js +21 -17
  19. package/dist-client/modeller/align.js.map +1 -1
  20. package/dist-client/modeller/editor.d.ts +8 -34
  21. package/dist-client/modeller/editor.js +175 -250
  22. package/dist-client/modeller/editor.js.map +1 -1
  23. package/dist-client/modeller/fasten-geometry.d.ts +3 -2
  24. package/dist-client/modeller/fasten-geometry.js +11 -9
  25. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  26. package/dist-client/modeller/figure-facts.d.ts +9 -1
  27. package/dist-client/modeller/figure-facts.js +11 -15
  28. package/dist-client/modeller/figure-facts.js.map +1 -1
  29. package/dist-client/modeller/figure-links.d.ts +4 -5
  30. package/dist-client/modeller/figure-links.js +14 -13
  31. package/dist-client/modeller/figure-links.js.map +1 -1
  32. package/dist-client/modeller/figure-preview.d.ts +5 -2
  33. package/dist-client/modeller/figure-preview.js +13 -4
  34. package/dist-client/modeller/figure-preview.js.map +1 -1
  35. package/dist-client/modeller/figure-properties.d.ts +3 -2
  36. package/dist-client/modeller/figure-properties.js +6 -6
  37. package/dist-client/modeller/figure-properties.js.map +1 -1
  38. package/dist-client/modeller/figure-verdict.d.ts +4 -2
  39. package/dist-client/modeller/figure-verdict.js +12 -9
  40. package/dist-client/modeller/figure-verdict.js.map +1 -1
  41. package/dist-client/modeller/joint-fan.d.ts +8 -4
  42. package/dist-client/modeller/joint-fan.js +15 -16
  43. package/dist-client/modeller/joint-fan.js.map +1 -1
  44. package/dist-client/modeller/links.d.ts +6 -5
  45. package/dist-client/modeller/links.js +17 -24
  46. package/dist-client/modeller/links.js.map +1 -1
  47. package/dist-client/modeller/live.d.ts +3 -2
  48. package/dist-client/modeller/live.js +7 -3
  49. package/dist-client/modeller/live.js.map +1 -1
  50. package/dist-client/modeller/motion-ends.d.ts +2 -2
  51. package/dist-client/modeller/motion-ends.js +5 -6
  52. package/dist-client/modeller/motion-ends.js.map +1 -1
  53. package/dist-client/modeller/occupancy.d.ts +2 -2
  54. package/dist-client/modeller/occupancy.js +2 -3
  55. package/dist-client/modeller/occupancy.js.map +1 -1
  56. package/dist-client/modeller/product-session.d.ts +44 -39
  57. package/dist-client/modeller/product-session.js +17 -0
  58. package/dist-client/modeller/product-session.js.map +1 -1
  59. package/dist-client/modeller/revision.d.ts +37 -0
  60. package/dist-client/modeller/revision.js +39 -0
  61. package/dist-client/modeller/revision.js.map +1 -0
  62. package/dist-client/pages/figure-modeller-page.d.ts +0 -6
  63. package/dist-client/pages/figure-modeller-page.js +6 -14
  64. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  65. package/dist-client/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +7 -7
  67. package/test/align.test.ts +14 -12
  68. package/test/browser/modeller-page.js +52 -4
  69. package/test/browser/modeller-revision.check.mjs +144 -0
  70. package/test/command-preview.test.ts +9 -8
  71. package/test/fasten-drawing.test.ts +1 -1
  72. package/test/fasten-geometry.test.ts +10 -9
  73. package/test/fields.test.ts +1 -1
  74. package/test/first-minute.test.ts +1 -1
  75. package/test/gizmo.test.ts +1 -1
  76. package/test/joint-fan.test.ts +30 -14
  77. package/test/joint-frames.test.ts +9 -8
  78. package/test/links.test.ts +13 -12
  79. package/test/live.test.ts +3 -3
  80. package/test/parts-wiring.test.ts +1 -1
  81. package/test/properties.test.ts +6 -5
  82. package/test/release-panel.test.ts +3 -3
  83. package/test/resize-with-placement.test.ts +8 -6
  84. package/test/revision-of.ts +22 -0
  85. package/test/revision.test.ts +172 -0
  86. package/test/section-editor.test.ts +4 -3
  87. package/test/section-panel.test.ts +5 -4
  88. package/test/verdict.test.ts +1 -1
  89. package/client/modeller/evaluated.ts +0 -47
  90. package/dist-client/modeller/evaluated.d.ts +0 -12
  91. package/dist-client/modeller/evaluated.js +0 -42
  92. package/dist-client/modeller/evaluated.js.map +0 -1
  93. package/dist-client/tsconfig.scratch.tsbuildinfo +0 -1
  94. package/test/evaluated.test.ts +0 -40
  95. package/test/mode-switch.test.ts +0 -43
@@ -7,7 +7,7 @@ import { notify } from '@operato/layout';
7
7
  import { OxPrompt } from '@operato/popup';
8
8
  import { customElement, property, state } from 'lit/decorators.js';
9
9
  import { live } from 'lit/directives/live.js';
10
- import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, parseFigureAsset, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figurePartFaceOf, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf, seedFigureEvaluation, upgradeFigurePlacement } from '@hatiolab/figure-model';
10
+ import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figurePartFaceOf, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
11
11
  import './figure-recipe-catalog.js';
12
12
  import { captureFigureThumbnail } from './thumbnail.js';
13
13
  import { drawableFigureParts } from './renderer.js';
@@ -30,7 +30,7 @@ import { keyActionOf } from './keys.js';
30
30
  import { groupFields, ownerOf, partLabelOf, repeatOf } from './fields.js';
31
31
  import { SidePanelStyles } from './side-panel-styles.js';
32
32
  import { ScrollbarStyles } from '@operato/styles';
33
- import { alignChanges, boxOf, canMirror, mirrorChanges } from './align.js';
33
+ import { alignChanges, canMirror, mirrorChanges } from './align.js';
34
34
  import { paletteRecipe } from './palette.js';
35
35
  import { apexOver, drawnOf } from './section-drawing.js';
36
36
  import { END_FACE } from './section-controls.js';
@@ -39,7 +39,7 @@ import { roomRefitRowsOf } from './occupancy.js';
39
39
  import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './refusals.js';
40
40
  import { endPlaceKeyOf, fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
41
41
  import { jointFanOf } from './joint-fan.js';
42
- import { seedEvaluatedGeometry } from './evaluated.js';
42
+ import { derivedHeld, derivedOf } from './revision.js';
43
43
  import { jointDragOf } from './joint-drag.js';
44
44
  import { liveOverSession, NOTHING_LIVE, scrubbed, scrubStep } from './live.js';
45
45
  import { startingShapeKindOfBody } from './starting-shape.js';
@@ -58,16 +58,9 @@ function foldSaid() {
58
58
  /** Product editor surface. The injected session is shared by page persistence and AI commands. */
59
59
  /** The words a panel is given when there are none: one array, so it does not read as a change. */
60
60
  const NO_WORDS = [];
61
+ const NO_OVERHANG = { words: NO_WORDS, note: NO_WORDS };
61
62
  /** How long a command's candidate rests before its preview is worked out. */
62
63
  const PREVIEW_SETTLE_MS = 150;
63
- function deepFreeze(value) {
64
- if (value && typeof value === 'object' && !Object.isFrozen(value)) {
65
- Object.freeze(value);
66
- for (const inner of Object.values(value))
67
- deepFreeze(inner);
68
- }
69
- return value;
70
- }
71
64
  let FigureEditor = class FigureEditor extends LitElement {
72
65
  constructor() {
73
66
  super(...arguments);
@@ -709,33 +702,36 @@ let FigureEditor = class FigureEditor extends LitElement {
709
702
  this.live = NOTHING_LIVE;
710
703
  }
711
704
  }
705
+ /** The bodies drawn with the value in hand, or null when nothing is in hand (or the figure cannot take it). */
706
+ liveParts(shown) {
707
+ return shown ? liveOverSession(shown, this.live) : null;
708
+ }
712
709
  /** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
713
- drawnParts(geometry, asset) {
714
- const parts = (asset ? liveOverSession(asset, this.currentInputs(), this.live) : null) ?? drawableFigureParts(geometry ?? []);
710
+ drawnParts(geometry, live) {
711
+ const parts = live ?? drawableFigureParts(geometry ?? []);
715
712
  return parts.filter(body => !this.hiddenParts.has(ownerOf(body.id)));
716
713
  }
717
714
  /**
718
715
  * Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
719
716
  * fan follows the arm while a control is dragged. Its size comes from what the joint turns (`joint-fan`).
720
717
  */
721
- fanOf(asset) {
722
- if (!asset || !this.selected || this.hiddenParts.has(this.selected))
718
+ fanOf(shown, live) {
719
+ if (!shown || !this.selected || this.hiddenParts.has(this.selected))
723
720
  return null;
724
721
  try {
725
- return jointFanOf(asset, this.selected, { ...this.currentInputs(), ...this.live.values });
722
+ return jointFanOf(shown, this.selected, live ? { values: this.live.values, geometry: live } : undefined);
726
723
  }
727
724
  catch {
728
725
  return null;
729
726
  }
730
727
  }
731
728
  /**
732
- * Every input as the session holds it now -- the pose the author has set with the controls, and design values. The
733
- * asset's source carries only the saved defaults, so drawing a value in hand over it put every other joint back to its
734
- * saved angle while one slider moved, and the arm jumped when released (user, 2026-09-24).
729
+ * The figure on screen at its version: what every panel and helper reads (`revision.ts`). Every input as the session
730
+ * holds it now is its `inputs` -- the pose set with the controls included: drawing a value in hand over the saved
731
+ * defaults put every other joint back to its saved angle while one slider moved (user, 2026-09-24).
735
732
  */
736
- currentInputs() {
737
- const view = this.session?.read();
738
- return view ? Object.fromEntries(view.fields.map(field => [field.id, field.value])) : {};
733
+ shown() {
734
+ return this.session?.revisionNow();
739
735
  }
740
736
  /** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
741
737
  hold(values, edit) {
@@ -864,7 +860,7 @@ let FigureEditor = class FigureEditor extends LitElement {
864
860
  */
865
861
  insertRecipe(recipe, values) {
866
862
  try {
867
- const asset = this.plainAsset();
863
+ const asset = this.shown().asset;
868
864
  const checked = parseFigureRecipe(recipe);
869
865
  const id = figureRecipeIdFor(asset, checked);
870
866
  const steps = resolveFigureRecipe(checked, { id, ...(checked.target === 'part' ? { target: this.selected } : {}), values });
@@ -881,7 +877,7 @@ let FigureEditor = class FigureEditor extends LitElement {
881
877
  /** The design values of the asset being edited. */
882
878
  designValues() {
883
879
  try {
884
- return this.session ? (this.plainAsset().designInputs ?? {}) : {};
880
+ return this.shown()?.asset.designInputs ?? {};
885
881
  }
886
882
  catch {
887
883
  return {};
@@ -894,7 +890,7 @@ let FigureEditor = class FigureEditor extends LitElement {
894
890
  if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id))
895
891
  return none;
896
892
  try {
897
- const asset = this.plainAsset();
893
+ const asset = this.shown().asset;
898
894
  const reach = gizmoReachOf(asset, id);
899
895
  /*
900
896
  A size drag shows the face it keeps and the commit keeps the same (figureScaleAnchorOf). Where figure-model cannot
@@ -922,9 +918,10 @@ let FigureEditor = class FigureEditor extends LitElement {
922
918
  transformed(id, result) {
923
919
  const view = this.session.read();
924
920
  /* A part hanging from a joint: the drag edits the joint -- its value, or where it sits (ADR-0093). */
925
- const asset = this.plainAsset();
921
+ const shown = this.shown();
922
+ const asset = shown.asset;
926
923
  const drawn = view.geometry.find(g => g.id === id);
927
- const drag = drawn ? jointDragOf(asset, ownerOf(id), drawn, result, this.currentInputs()) : null;
924
+ const drag = drawn ? jointDragOf(asset, ownerOf(id), drawn, result, shown.inputs) : null;
928
925
  if (drag) {
929
926
  if (drag.kind === 'value')
930
927
  this.run(() => this.session.apply([{ kind: 'set-state', input: drag.input, unit: drag.unit, value: drag.value }]));
@@ -1005,8 +1002,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1005
1002
  /** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
1006
1003
  align(tool) {
1007
1004
  this.run(() => {
1008
- const asset = this.plainAsset();
1009
- const changes = alignChanges(asset, this.selected, this.reachOf(this.selected), tool);
1005
+ const changes = alignChanges(this.shown(), this.selected, this.reachOf(this.selected), tool);
1010
1006
  if (changes.length)
1011
1007
  this.session.apply(changes);
1012
1008
  });
@@ -1020,7 +1016,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1020
1016
  const copy = this.session.read().geometry.find(g => !before.has(g.id))?.id;
1021
1017
  if (!copy)
1022
1018
  return;
1023
- this.session.apply(mirrorChanges(this.plainAsset(), id, copy, axis));
1019
+ this.session.apply(mirrorChanges(this.shown(), id, copy, axis));
1024
1020
  this.selected = copy;
1025
1021
  });
1026
1022
  }
@@ -1032,32 +1028,30 @@ let FigureEditor = class FigureEditor extends LitElement {
1032
1028
  */
1033
1029
  /* What a tilted part's typed size keeps, by the figure's placement: not the floor's lowest point everywhere (designer 2026-09-27). */
1034
1030
  keptExtremeWord() {
1035
- const placement = this.asset()?.placement ?? 'floor';
1031
+ const placement = (this.asset())?.placement ?? 'floor';
1036
1032
  return i18next.t(`figure.label.kept-extreme-${placement}`);
1037
1033
  }
1038
- planeContactOf(source, part) {
1039
- if (this.planeContact?.source !== source || this.planeContact.part !== part) {
1040
- let contact;
1034
+ /* How the selected part would meet the plane, worked out once per version and part. */
1035
+ planeContactOf(shown, part) {
1036
+ return derivedOf(shown, `plane-contact|${part}`, () => {
1041
1037
  try {
1042
- contact = figurePlaneAlignChanges(parseFigureAsset(source), part).contact;
1038
+ return figurePlaneAlignChanges(shown.asset, part).contact;
1043
1039
  }
1044
1040
  catch {
1045
- contact = undefined;
1041
+ return undefined;
1046
1042
  }
1047
- this.planeContact = { source, part, contact };
1048
- }
1049
- return this.planeContact.contact;
1043
+ });
1050
1044
  }
1051
- renderPlaneAlign(asset, unavailable) {
1052
- const placement = asset.placement ?? 'floor';
1045
+ renderPlaneAlign(shown, unavailable) {
1046
+ const placement = shown.asset.placement ?? 'floor';
1053
1047
  /* A tilted part touches the plane at a point or an edge, a ball at its lowest; the tooltip says so rather than promise a face (designer 2026-09-27). */
1054
- const contact = this.planeContactOf(this.session.source(), this.selected);
1048
+ const contact = this.planeContactOf(shown, this.selected);
1055
1049
  const told = contact === 'tilted' || contact === 'extreme' ? `-${contact}` : '';
1056
1050
  return html `<button plane-align=${placement} ?disabled=${unavailable} title=${i18next.t(`figure.text.align-to-${placement}${told}`)}
1057
1051
  @click=${() => this.alignToPlane()}>${i18next.t(`figure.button.align-to-${placement}`)}</button>`;
1058
1052
  }
1059
1053
  alignToPlane() {
1060
- const asset = this.parsedAsset();
1054
+ const asset = this.shown().asset;
1061
1055
  const { changes, contact, refusal } = figurePlaneAlignChanges(asset, this.selected);
1062
1056
  if (refusal) {
1063
1057
  this.error = i18next.t(`figure.text.align-refused-${refusal.toLowerCase().replace(/_/g, '-')}`, { part: partLabelOf(this.selected) });
@@ -1070,27 +1064,21 @@ let FigureEditor = class FigureEditor extends LitElement {
1070
1064
  }
1071
1065
  renderTools(unavailable) {
1072
1066
  const reach = this.reachOf(this.selected);
1073
- if (!this.selected || !reach.translate)
1067
+ const shown = this.shown();
1068
+ if (!this.selected || !reach.translate || !shown)
1074
1069
  return null;
1075
- let asset;
1076
- try {
1077
- asset = this.plainAsset();
1078
- }
1079
- catch {
1080
- return null;
1081
- }
1082
1070
  /*
1083
1071
  * In words, two rows by what they do (user 2026-10-01: 「무엇을 하는 것인지 모르겠다」): moving the part to the figure's
1084
1072
  * centre lines, onto its plane or onto the grid; and making a mirrored copy, which makes a part and moves nothing.
1085
1073
  */
1086
1074
  const tool = (name, label, title) => html `<button ?disabled=${unavailable} title=${i18next.t(title)} @click=${() => this.align(name)}>${i18next.t(label)}</button>`;
1087
- const mirrors = ['x', 'z'].map(axis => ({ axis, able: canMirror(asset, this.selected, axis, reach) }));
1075
+ const mirrors = ['x', 'z'].map(axis => ({ axis, able: canMirror(shown, this.selected, axis, reach) }));
1088
1076
  return html `<div vector-row>
1089
1077
  <label>${i18next.t('figure.label.tools')}</label>
1090
1078
  <div tools>
1091
1079
  ${tool('centre-x', 'figure.button.center-x', 'figure.text.center-on-x')}
1092
1080
  ${tool('centre-z', 'figure.button.center-z', 'figure.text.center-on-z')}
1093
- ${this.renderPlaneAlign(asset, unavailable)}
1081
+ ${this.renderPlaneAlign(shown, unavailable)}
1094
1082
  ${tool('snap', 'figure.button.snap', 'figure.text.snap-explained')}
1095
1083
  </div>
1096
1084
  </div>
@@ -1140,21 +1128,6 @@ let FigureEditor = class FigureEditor extends LitElement {
1140
1128
  : String(error);
1141
1129
  }
1142
1130
  }
1143
- dimsOf(asset) {
1144
- const view = this.session?.read();
1145
- const key = `${view?.context}|${view?.revision}|${this.selected}`;
1146
- if (this.dimsSaid?.key === key)
1147
- return this.dimsSaid.dims;
1148
- let dims;
1149
- try {
1150
- dims = boxOf(asset, this.selected, this.currentInputs())?.dims;
1151
- }
1152
- catch {
1153
- dims = undefined;
1154
- }
1155
- this.dimsSaid = { key, dims };
1156
- return dims;
1157
- }
1158
1131
  previewWordsNow() {
1159
1132
  const preview = this.currentPreview();
1160
1133
  const language = i18next.language;
@@ -1165,25 +1138,19 @@ let FigureEditor = class FigureEditor extends LitElement {
1165
1138
  return words;
1166
1139
  }
1167
1140
  renderLinks(unavailable, section) {
1168
- let asset;
1169
- try {
1170
- asset = this.plainAsset();
1171
- }
1172
- catch {
1141
+ const shown = this.shown();
1142
+ if (!shown)
1173
1143
  return null;
1174
- }
1175
- const dims = this.dimsOf(asset);
1176
1144
  return html `<figure-links
1177
- .asset=${asset}
1145
+ .revision=${shown}
1178
1146
  .section=${section}
1179
1147
  .partId=${this.selected}
1180
- .dims=${dims}
1181
1148
  ?locked=${unavailable}
1182
1149
  @command=${(e) => this.linkCommand(e.detail.action)}
1183
1150
  .previewWords=${this.previewWordsNow()}
1184
- .detachWords=${this.detachWords(asset)}
1185
- .overhangWords=${section === 'shape' ? this.overhangWords(asset) : NO_WORDS}
1186
- .overhangNote=${section === 'shape' ? this.overhangNote(asset) : NO_WORDS}
1151
+ .detachWords=${this.detachWords(shown)}
1152
+ .overhangWords=${section === 'shape' ? this.overhangOf(shown).words : NO_WORDS}
1153
+ .overhangNote=${section === 'shape' ? this.overhangOf(shown).note : NO_WORDS}
1187
1154
  .motionEnds=${section === 'motion' ? this.motionEnds() : null}
1188
1155
  @go-fasten=${() => this.goFasten()}
1189
1156
  @preview-command=${(e) => this.previewCommand(e.detail)}
@@ -1212,13 +1179,9 @@ let FigureEditor = class FigureEditor extends LitElement {
1212
1179
  }
1213
1180
  /** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
1214
1181
  renderControls(view, unavailable) {
1215
- let asset;
1216
- try {
1217
- asset = this.plainAsset();
1218
- }
1219
- catch {
1182
+ const asset = this.shown()?.asset;
1183
+ if (!asset)
1220
1184
  return null;
1221
- }
1222
1185
  const values = Object.fromEntries(view.fields.filter(f => f.role === 'state').map(f => [f.id, f.value]));
1223
1186
  return html `<figure-controls
1224
1187
  .asset=${asset}
@@ -1303,7 +1266,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1303
1266
  /* A size typed in keeps the face the figure's placement keeps -- figure-model's one rule, as the gizmo and "follow the size" do. */
1304
1267
  const part = this.selected;
1305
1268
  const change = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value };
1306
- this.run(() => this.session.apply(field.role !== 'state' && part ? figureSizeEditChanges(this.parsedAsset(), part, [change]) : [change]));
1269
+ this.run(() => this.session.apply(field.role !== 'state' && part ? figureSizeEditChanges(this.shown().asset, part, [change]) : [change]));
1307
1270
  // Rejected edits must also restore the displayed numeric value.
1308
1271
  input.value = String(this.session.read().fields.find(i => i.id === field.id).value);
1309
1272
  }
@@ -1353,12 +1316,12 @@ let FigureEditor = class FigureEditor extends LitElement {
1353
1316
  label.addEventListener('pointercancel', up);
1354
1317
  }
1355
1318
  /** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
1356
- renderInstanceRange(asset, unavailable) {
1357
- if (!asset)
1319
+ renderInstanceRange(shown, unavailable) {
1320
+ if (!shown)
1358
1321
  return nothing;
1359
- /* Another figure, or another step of this one, is measured again. */
1360
- const at = `${this.session?.read().context ?? ''}:${this.revision}`;
1361
- if (this.range?.at !== at) {
1322
+ const asset = shown.asset;
1323
+ /* Measured once per version: measuring evaluates the figure at its edges. */
1324
+ const measured = derivedOf(shown, 'instance-range', () => {
1362
1325
  let of;
1363
1326
  let toDeclare;
1364
1327
  try {
@@ -1373,11 +1336,11 @@ let FigureEditor = class FigureEditor extends LitElement {
1373
1336
  catch {
1374
1337
  toDeclare = undefined;
1375
1338
  }
1376
- this.range = { at, of, toDeclare };
1377
- }
1378
- const range = this.range.of;
1339
+ return { of, toDeclare };
1340
+ });
1341
+ const range = measured.of;
1379
1342
  /* No size yet: the figure is declared its size from its parts, so a board can resize it and parts can follow it. */
1380
- const toDeclare = this.range.toDeclare;
1343
+ const toDeclare = measured.toDeclare;
1381
1344
  if (!range && toDeclare)
1382
1345
  return html `<section>
1383
1346
  <h3>${i18next.t('figure.label.instance-range')}</h3>
@@ -1492,9 +1455,10 @@ let FigureEditor = class FigureEditor extends LitElement {
1492
1455
  * The pitch of a row that fills its length is edited as a number (`set-repeat` makes it the part's own the first
1493
1456
  * time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
1494
1457
  */
1495
- renderRepeat(asset, geometry, unavailable) {
1458
+ renderRepeat(shown, geometry, unavailable) {
1496
1459
  const repeat = repeatOf(this.selected, geometry);
1497
- if (!asset || !repeat || repeat.index)
1460
+ const asset = shown?.asset;
1461
+ if (!shown || !asset || !repeat || repeat.index)
1498
1462
  return nothing;
1499
1463
  /* The row is set on its part; what hangs from that part is copied with it and has no row of its own. */
1500
1464
  if (!asset.document.model.nodes.some(n => n.id === `${this.selected}.repeat`))
@@ -1504,7 +1468,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1504
1468
  const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
1505
1469
  const own = `${this.selected}.repeat.pitch`;
1506
1470
  /* The pitch as it stands: the part's own number, a constant, or what a pitch that follows a length comes to. */
1507
- const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(asset, layout?.args[4]);
1471
+ const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(shown, layout?.args[4]);
1508
1472
  const fits = layout?.op === 'fit-pitch@1';
1509
1473
  /* What the pitch gives, in numbers: this many copies at the length it is authored, and how long it may be placed. */
1510
1474
  const lengthRef = layout?.args[0] ?? '';
@@ -1527,7 +1491,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1527
1491
  @change=${(e) => this.setPitch(field, e)}
1528
1492
  />
1529
1493
  </div>
1530
- <ul row-reading>${rowWordsOf(this.rowNow(asset), (key, params) => i18next.t(key, params)).map(line => html `<li>${line}</li>`)}</ul>
1494
+ <ul row-reading>${rowWordsOf(this.rowNow(shown), (key, params) => i18next.t(key, params)).map(line => html `<li>${line}</li>`)}</ul>
1531
1495
  <p hint>
1532
1496
  ${fits && length !== undefined && longest !== undefined && limit !== undefined
1533
1497
  ? i18next.t('figure.text.repeat-what-pitch-gives', {
@@ -1547,13 +1511,15 @@ let FigureEditor = class FigureEditor extends LitElement {
1547
1511
  </section>`;
1548
1512
  }
1549
1513
  /** The selected row as it is drawn now, read by figure-model: nothing when it cannot be read. */
1550
- rowNow(asset) {
1551
- try {
1552
- return figureRowOf(asset, this.selected, this.currentInputs());
1553
- }
1554
- catch {
1555
- return undefined;
1556
- }
1514
+ rowNow(shown) {
1515
+ return derivedOf(shown, `row|${this.selected}`, () => {
1516
+ try {
1517
+ return figureRowOf(shown.asset, this.selected, shown.inputs);
1518
+ }
1519
+ catch {
1520
+ return undefined;
1521
+ }
1522
+ });
1557
1523
  }
1558
1524
  /** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
1559
1525
  pitchInputOf(asset, part) {
@@ -1641,7 +1607,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1641
1607
  if (!Number.isFinite(pitch) || pitch === field.value)
1642
1608
  return;
1643
1609
  /* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
1644
- const from = this.pitchInputOf(this.plainAsset(), this.selected);
1610
+ const from = this.pitchInputOf(this.shown().asset, this.selected);
1645
1611
  if (from) {
1646
1612
  this.run(() => this.session.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]));
1647
1613
  input.value = String(this.session.read().fields.find(i => i.id === from)?.value ?? field.value);
@@ -1699,14 +1665,15 @@ let FigureEditor = class FigureEditor extends LitElement {
1699
1665
  let face;
1700
1666
  if (candidate.face) {
1701
1667
  try {
1702
- face = figurePartFaceOf(this.parsedAsset(), candidate.face.part, candidate.face.face, this.currentInputs());
1668
+ const shown = this.shown();
1669
+ face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs);
1703
1670
  }
1704
1671
  catch {
1705
1672
  face = undefined;
1706
1673
  }
1707
1674
  }
1708
1675
  /* A row is read on the figure the command would make, by figure-model, as drawn. */
1709
- const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.currentInputs()) : undefined;
1676
+ const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.shown().inputs) : undefined;
1710
1677
  this.preview = {
1711
1678
  ...held,
1712
1679
  context: seen.context,
@@ -1738,37 +1705,36 @@ let FigureEditor = class FigureEditor extends LitElement {
1738
1705
  * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
1739
1706
  * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
1740
1707
  */
1741
- detachWords(asset) {
1742
- const view = this.session?.read();
1743
- const joint = asset?.joints?.find(j => j.body1.part === this.selected);
1744
- if (!asset || !view || !joint || !this.session)
1708
+ detachWords(shown) {
1709
+ const part = this.selected;
1710
+ if (!this.session || !shown.asset.joints?.some(j => j.body1.part === part))
1745
1711
  return NO_WORDS;
1746
- const key = `${view.context}|${view.revision}|${this.selected}`;
1747
- if (this.detachSaid?.key === key)
1748
- return this.detachSaid.words;
1749
- if (this.detachAsked !== key) {
1750
- this.detachAsked = key;
1712
+ const key = `detach|${part}`;
1713
+ const said = derivedHeld(shown, key);
1714
+ if (said)
1715
+ return said;
1716
+ if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {
1717
+ this.detachAsked = { shown, part };
1751
1718
  clearTimeout(this.detachTimer);
1752
1719
  this.detachTimer = setTimeout(() => {
1753
- const now = this.session?.read();
1754
- if (!now || `${now.context}|${now.revision}|${this.selected}` !== key)
1720
+ if (this.shown() !== shown || this.selected !== part)
1755
1721
  return;
1756
- this.computeDetachWords(asset, key);
1722
+ derivedOf(shown, key, () => this.computeDetachWords(shown, part));
1757
1723
  this.requestUpdate();
1758
1724
  }, PREVIEW_SETTLE_MS);
1759
1725
  }
1760
1726
  return NO_WORDS;
1761
1727
  }
1762
- computeDetachWords(asset, key) {
1728
+ computeDetachWords(shown, part) {
1763
1729
  const say = (k, params) => i18next.t(k, params);
1764
1730
  let words;
1765
1731
  try {
1766
- const seen = this.session.previewCommand({ kind: 'detach', part: this.selected });
1767
- const own = seen.moves.some(m => m.body === this.selected);
1732
+ const seen = this.session.previewCommand({ kind: 'detach', part });
1733
+ const own = seen.moves.some(m => m.body === part);
1768
1734
  words = own
1769
- ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key, moves: seen.moves }, this.selected, say)]
1735
+ ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]
1770
1736
  : /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ [];
1771
- const children = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === this.selected).map(j => partLabelOf(j.body1.part));
1737
+ const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part));
1772
1738
  if (children.length)
1773
1739
  words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }));
1774
1740
  if (seen.clipsWouldGo.length)
@@ -1777,67 +1743,62 @@ let FigureEditor = class FigureEditor extends LitElement {
1777
1743
  catch (error) {
1778
1744
  words = [say('figure.text.detach-refused', { reason: this.said(error) })];
1779
1745
  }
1780
- this.detachSaid = { key, words };
1781
1746
  return words;
1782
1747
  }
1783
- /** Wider than its face: a fact of the relation, said in its details, not as a warning (designer 2026-09-27). */
1784
- overhangNote(asset) {
1785
- this.overhangWords(asset);
1786
- return this.overhangSaid?.note ?? [];
1787
- }
1788
- overhangWords(asset) {
1789
- const view = this.session?.read();
1790
- if (!asset || !view || !this.selected)
1791
- return NO_WORDS;
1792
- const key = `${view.context}|${view.revision}|${this.selected}`;
1793
- if (this.overhangSaid?.key === key)
1794
- return this.overhangSaid.words;
1795
- const say = (k, params) => i18next.t(k, params);
1796
- const where = (o) => ({ on: partLabelOf(o.on.part), face: say(`figure.label.face-${o.on.face}`) });
1797
- const sides = (o) => o.sides.map(s => say(`figure.text.overhang-side-${s.side}`, { axis: s.axis.toUpperCase(), mm: String(s.mm) })).join(', ');
1798
- const words = [];
1799
- const note = [];
1800
- try {
1801
- const inputs = this.currentInputs();
1802
- const own = figureFaceOverhangOf(asset, this.selected, inputs);
1803
- if (own?.state === 'larger') {
1804
- note.push(say('figure.text.overhang-larger', { ...where(own), sides: sides(own) }));
1805
- if (own.lopsided.length)
1806
- note.push(say('figure.text.overhang-lopsided', { axes: own.lopsided.map(a => a.toUpperCase()).join(', ') }));
1748
+ /*
1749
+ Past the edge of the face a part is fastened to (ADR-0097 §5.5, Q3): the selected part's own, and the parts fastened
1750
+ to it -- said apart from the room, nothing moved. Read once per version, part and language. The note says it is wider
1751
+ than its face: a fact of the relation, said in its details, not as a warning (designer 2026-09-27).
1752
+ */
1753
+ overhangOf(shown) {
1754
+ const part = this.selected;
1755
+ if (!part)
1756
+ return NO_OVERHANG;
1757
+ return derivedOf(shown, `overhang|${part}|${i18next.language}`, () => {
1758
+ const asset = shown.asset;
1759
+ const say = (k, params) => i18next.t(k, params);
1760
+ const where = (o) => ({ on: partLabelOf(o.on.part), face: say(`figure.label.face-${o.on.face}`) });
1761
+ const sides = (o) => o.sides.map(s => say(`figure.text.overhang-side-${s.side}`, { axis: s.axis.toUpperCase(), mm: String(s.mm) })).join(', ');
1762
+ const words = [];
1763
+ const note = [];
1764
+ try {
1765
+ const own = figureFaceOverhangOf(asset, part, shown.inputs);
1766
+ if (own?.state === 'larger') {
1767
+ note.push(say('figure.text.overhang-larger', { ...where(own), sides: sides(own) }));
1768
+ if (own.lopsided.length)
1769
+ note.push(say('figure.text.overhang-lopsided', { axes: own.lopsided.map(a => a.toUpperCase()).join(', ') }));
1770
+ }
1771
+ if (own?.state === 'out')
1772
+ words.push(say('figure.text.overhang-out', { ...where(own), sides: sides(own) }));
1773
+ else if (own?.state === 'possible')
1774
+ words.push(say('figure.text.overhang-possible', where(own)));
1775
+ else if (own?.state === 'unknown')
1776
+ words.push(say('figure.text.overhang-unknown', where(own)));
1777
+ const held = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => figureFaceOverhangOf(asset, j.body1.part, shown.inputs));
1778
+ const past = held.filter((o) => o?.state === 'out' || o?.state === 'possible');
1779
+ if (past.length)
1780
+ words.push(say('figure.text.overhang-held', { parts: past.map(o => partLabelOf(o.part)).join(', ') }));
1807
1781
  }
1808
- if (own?.state === 'out')
1809
- words.push(say('figure.text.overhang-out', { ...where(own), sides: sides(own) }));
1810
- else if (own?.state === 'possible')
1811
- words.push(say('figure.text.overhang-possible', where(own)));
1812
- else if (own?.state === 'unknown')
1813
- words.push(say('figure.text.overhang-unknown', where(own)));
1814
- const held = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === this.selected).map(j => figureFaceOverhangOf(asset, j.body1.part, inputs));
1815
- const past = held.filter((o) => o?.state === 'out' || o?.state === 'possible');
1816
- if (past.length)
1817
- words.push(say('figure.text.overhang-held', { parts: past.map(o => partLabelOf(o.part)).join(', ') }));
1818
- }
1819
- catch {
1820
- /* Not read: nothing said rather than a guess. */
1821
- }
1822
- this.overhangSaid = { key, words, note };
1823
- return words;
1782
+ catch {
1783
+ /* Not read: nothing said rather than a guess. */
1784
+ }
1785
+ return { words, note };
1786
+ });
1824
1787
  }
1788
+ /** The selected part's motion at both ends of its range, made once per version and part. */
1825
1789
  motionEnds() {
1826
- const view = this.session?.read();
1827
- if (!view || !this.selected)
1790
+ const shown = this.shown();
1791
+ const part = this.selected;
1792
+ if (!shown || !part)
1828
1793
  return null;
1829
- const key = `${view.context}|${view.revision}|${this.selected}`;
1830
- if (this.motionEndsSaid?.key !== key) {
1831
- let ends = null;
1794
+ return derivedOf(shown, `motion-ends|${part}`, () => {
1832
1795
  try {
1833
- ends = motionEndsOf(this.parsedAsset(), this.selected, this.currentInputs());
1796
+ return motionEndsOf(shown, part);
1834
1797
  }
1835
1798
  catch {
1836
- ends = null;
1799
+ return null;
1837
1800
  }
1838
- this.motionEndsSaid = { key, ends };
1839
- }
1840
- return this.motionEndsSaid.ends;
1801
+ });
1841
1802
  }
1842
1803
  /** "Fasten it first", from the motion tab: to the properties tab, the fastening form open on this part. */
1843
1804
  async goFasten() {
@@ -1874,50 +1835,8 @@ let FigureEditor = class FigureEditor extends LitElement {
1874
1835
  if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
1875
1836
  this.tab = 'properties';
1876
1837
  }
1877
- /**
1878
- * The figure as the session holds it now, parsed once per text: a render asks for it a dozen times, and parsing the whole
1879
- * figure is most of what a selection costs. Read, never changed -- what changes a figure goes through the session.
1880
- */
1881
- /**
1882
- * The session has evaluated this figure at this version, with these inputs: the screen's questions of the text it just
1883
- * parsed are answered from that, not by evaluating the figure again (each is tens of milliseconds on a real figure).
1884
- */
1885
- seeded(asset) {
1886
- const view = this.session?.read();
1887
- if (view?.geometry.length) {
1888
- const values = this.currentInputs();
1889
- seedEvaluatedGeometry(asset, values, view.geometry);
1890
- seedFigureEvaluation(asset, values, view.geometry);
1891
- }
1892
- return asset;
1893
- }
1894
- parsedAsset() {
1895
- const text = this.session.source();
1896
- if (this.parsed?.text !== text) {
1897
- /* The session's text is what its workspace exported, compiled then: it is read here, not compiled again. */
1898
- const plain = this.plainAsset();
1899
- const upgraded = upgradeFigurePlacement(plain);
1900
- this.parsed = { text, asset: upgraded === plain ? plain : this.seeded(deepFreeze(upgraded)) };
1901
- }
1902
- return this.parsed.asset;
1903
- }
1904
- /**
1905
- * The figure as plain data, parsed once per text and frozen: the render, the gizmo's reach and every tool read it, and
1906
- * a copy for each of them cost more than the selection they serve. A change goes through the session, never through this.
1907
- */
1908
- plainAsset() {
1909
- const text = this.session.source();
1910
- if (this.plain?.text !== text)
1911
- this.plain = { text, asset: this.seeded(deepFreeze(JSON.parse(text))) };
1912
- return this.plain.asset;
1913
- }
1914
1838
  asset() {
1915
- try {
1916
- return this.plainAsset();
1917
- }
1918
- catch {
1919
- return undefined;
1920
- }
1839
+ return this.shown()?.asset;
1921
1840
  }
1922
1841
  /** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
1923
1842
  renderTab(tab, icon, label, badge) {
@@ -1984,27 +1903,32 @@ let FigureEditor = class FigureEditor extends LitElement {
1984
1903
  @section-editor-close=${() => this.closeSectionEditor()}
1985
1904
  ></figure-section-editor>`;
1986
1905
  }
1906
+ /**
1907
+ * Why a drawn push's end cannot be drawn to one point now: the kernel's own refusal of that command on a copy (what is
1908
+ * fastened to the end face), said as the face. Asked once per figure revision and part.
1909
+ */
1987
1910
  pointBlockedOf(drawn) {
1988
- if (drawn.apex || drawn.section.kind !== 'polygon' || !this.session)
1911
+ const shown = this.shown();
1912
+ const session = this.session;
1913
+ if (drawn.apex || drawn.section.kind !== 'polygon' || !session || !shown)
1989
1914
  return '';
1990
- const key = `${this.session.read().revision}|${this.selected}`;
1991
- if (this.pointBlocked?.key === key)
1992
- return this.pointBlocked.words;
1993
- let words = '';
1994
- try {
1995
- this.session.previewCommand({ kind: 'set-section', part: this.selected, apex: apexOver(drawn.section.points, drawn.axis) });
1996
- }
1997
- catch (error) {
1998
- if (error.code === 'FEATURE_MAPPING_REQUIRED')
1999
- words = i18next.t('figure.text.section-editor-apex-blocked', { face: i18next.t('figure.label.face-' + END_FACE(drawn.axis, 'end')) });
2000
- }
2001
- this.pointBlocked = { key, words };
2002
- return words;
1915
+ const part = this.selected;
1916
+ const apex = apexOver(drawn.section.points, drawn.axis);
1917
+ return derivedOf(shown, `point-blocked|${part}|${i18next.language}`, () => {
1918
+ try {
1919
+ session.previewCommand({ kind: 'set-section', part, apex });
1920
+ }
1921
+ catch (error) {
1922
+ if (error.code === 'FEATURE_MAPPING_REQUIRED')
1923
+ return i18next.t('figure.text.section-editor-apex-blocked', { face: i18next.t('figure.label.face-' + END_FACE(drawn.axis, 'end')) });
1924
+ }
1925
+ return '';
1926
+ });
2003
1927
  }
2004
1928
  renderStart() {
2005
1929
  const shapes = ['box', 'cylinder', 'plate'];
2006
1930
  /* Where it is authored from, chosen before any part (ADR-0097): the figure's own setting, no room made up for it. */
2007
- const placement = (this.asset()?.placement ?? 'floor');
1931
+ const placement = ((this.asset())?.placement ?? 'floor');
2008
1932
  return html `<div start role="region" aria-label=${i18next.t('figure.text.start-question')}>
2009
1933
  <h2>${i18next.t('figure.text.start-question')}</h2>
2010
1934
  <div start-paths>
@@ -2047,7 +1971,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2047
1971
  let summary;
2048
1972
  try {
2049
1973
  summary = this.selected
2050
- ? partSummaryOf(this.parsedAsset(), ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
1974
+ ? partSummaryOf(this.shown().asset, ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
2051
1975
  : undefined;
2052
1976
  }
2053
1977
  catch {
@@ -2085,9 +2009,11 @@ let FigureEditor = class FigureEditor extends LitElement {
2085
2009
  A part's field only the declared room still reads -- its old height position once the floor holds it -- moves
2086
2010
  nothing; it is shown apart as what it is, not as a position (ADR-0097, designer 2026-09-27).
2087
2011
  */
2012
+ const shown = this.shown();
2013
+ const asset = shown?.asset;
2088
2014
  let roomOnly = [];
2089
2015
  try {
2090
- roomOnly = this.selected ? figureRoomOnlyInputsOf(this.parsedAsset(), this.selected) : [];
2016
+ roomOnly = this.selected && asset ? figureRoomOnlyInputsOf(asset, this.selected) : [];
2091
2017
  }
2092
2018
  catch {
2093
2019
  roomOnly = [];
@@ -2098,14 +2024,13 @@ let FigureEditor = class FigureEditor extends LitElement {
2098
2024
  try {
2099
2025
  const body = view.geometry.find(g => g.id === this.selected);
2100
2026
  const placed = body ? placementOf(body) : undefined;
2101
- const parsed = this.selected ? this.parsedAsset() : undefined;
2102
- if (placed && parsed)
2027
+ if (placed && shown && this.selected)
2103
2028
  held = {
2104
2029
  position: placed.position,
2105
2030
  rotation: placed.rotation,
2106
- size: ['x', 'y', 'z'].map(axis => measureNow(parsed, ownerOf(this.selected), axis)),
2031
+ size: ['x', 'y', 'z'].map(axis => measureNow(shown, ownerOf(this.selected), axis)),
2107
2032
  /* Only a size a rule sets: a palette part's own numbers are its shape's fields, not a rule's (found in the browser). */
2108
- sizeRuled: ['x', 'y', 'z'].every(axis => followOf(parsed, ownerOf(this.selected), axis).of !== 'own')
2033
+ sizeRuled: ['x', 'y', 'z'].every(axis => followOf(shown.asset, ownerOf(this.selected), axis).of !== 'own')
2109
2034
  };
2110
2035
  }
2111
2036
  catch {
@@ -2119,8 +2044,8 @@ let FigureEditor = class FigureEditor extends LitElement {
2119
2044
  : view.dirty || !view.id
2120
2045
  ? 'figure.text.unsaved-changes'
2121
2046
  : 'figure.text.saved';
2122
- const asset = this.asset();
2123
2047
  const tab = this.tab;
2048
+ const live = this.liveParts(shown);
2124
2049
  return html `<ox-canvas-bar
2125
2050
  history
2126
2051
  fit
@@ -2181,7 +2106,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2181
2106
  ? html `<figure-preview
2182
2107
  ?hidden=${this.middle !== 'preview'}
2183
2108
  .active=${this.middle === 'preview'}
2184
- .asset=${asset}
2109
+ .revision=${shown}
2185
2110
  .context=${view.context ?? ''}
2186
2111
  .view=${this.view}
2187
2112
  .selected=${this.selected}
@@ -2200,9 +2125,9 @@ let FigureEditor = class FigureEditor extends LitElement {
2200
2125
  ${this.sectionEditing && this.sectionEditing === this.selected && !unavailable ? this.renderSectionEditor(view.geometry) : nothing}
2201
2126
  <figure-canvas
2202
2127
  .view=${this.view}
2203
- .parts=${this.drawnParts(view.geometry, asset)}
2128
+ .parts=${this.drawnParts(view.geometry, live)}
2204
2129
  .anchors=${this.anchorsOf(asset)}
2205
- .fan=${this.fanOf(asset)}
2130
+ .fan=${this.fanOf(shown, live)}
2206
2131
  .ghost=${this.previewShown().ghost}
2207
2132
  .highlight=${this.previewShown().highlight}
2208
2133
  .palette=${view.palette ?? {}}
@@ -2242,7 +2167,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2242
2167
  </div>
2243
2168
  ${this.renderRepeatNote(view.geometry)}
2244
2169
  </section>
2245
- ${this.renderRepeat(asset, view.geometry, unavailable)}
2170
+ ${this.renderRepeat(shown, view.geometry, unavailable)}
2246
2171
  ${this.renderGroups(this.withSizeRow(groupFields(partFields, this.selected), held), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable), held)}
2247
2172
  ${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}
2248
2173
  ${this.renderLinks(unavailable, 'shape')}
@@ -2273,7 +2198,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2273
2198
  </section>`
2274
2199
  : html `${this.renderSubject()}<slot name="figure"></slot>
2275
2200
  ${this.renderGroups(groupFields(common), 'figure.label.group-figure-size', 'figure.label.design-inputs', unavailable)}
2276
- ${this.renderInstanceRange(asset, unavailable)}`}
2201
+ ${this.renderInstanceRange(shown, unavailable)}`}
2277
2202
  </div>
2278
2203
  <div panel ?off=${tab !== 'capability'}>
2279
2204
  ${this.renderSubject()}
@@ -2369,7 +2294,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2369
2294
  * something else. A size that follows something is said at the panel's head, so not again here.
2370
2295
  */
2371
2296
  renderHeldAxes(vectors) {
2372
- const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(this.selected));
2297
+ const fastened = !!(this.asset())?.joints?.some(j => j.body1.part === ownerOf(this.selected));
2373
2298
  const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && ['x', 'y', 'z'].some(axis => !g.cells[axis]));
2374
2299
  if (!held.length)
2375
2300
  return nothing;
@@ -2396,7 +2321,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2396
2321
  async askRoomRefit() {
2397
2322
  let refit;
2398
2323
  try {
2399
- refit = figureRoomRefitOf(this.parsedAsset());
2324
+ refit = figureRoomRefitOf(this.shown().asset);
2400
2325
  }
2401
2326
  catch (error) {
2402
2327
  this.error = this.said(error);