@things-factory/figure-ui 10.1.123 → 10.1.126

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 (38) hide show
  1. package/client/modeller/align.ts +6 -4
  2. package/client/modeller/editor.ts +142 -32
  3. package/client/modeller/evaluated.ts +47 -0
  4. package/client/modeller/figure-facts.ts +8 -0
  5. package/client/modeller/figure-properties.ts +32 -0
  6. package/client/modeller/figure-verdict.ts +10 -1
  7. package/client/modeller/joint-fan.ts +4 -7
  8. package/client/modeller/links.ts +2 -1
  9. package/dist-client/modeller/align.d.ts +2 -1
  10. package/dist-client/modeller/align.js +6 -4
  11. package/dist-client/modeller/align.js.map +1 -1
  12. package/dist-client/modeller/clips.d.ts +1 -1
  13. package/dist-client/modeller/editor.d.ts +35 -1
  14. package/dist-client/modeller/editor.js +129 -33
  15. package/dist-client/modeller/editor.js.map +1 -1
  16. package/dist-client/modeller/evaluated.d.ts +12 -0
  17. package/dist-client/modeller/evaluated.js +42 -0
  18. package/dist-client/modeller/evaluated.js.map +1 -0
  19. package/dist-client/modeller/figure-facts.d.ts +0 -7
  20. package/dist-client/modeller/figure-facts.js +9 -0
  21. package/dist-client/modeller/figure-facts.js.map +1 -1
  22. package/dist-client/modeller/figure-properties.d.ts +8 -2
  23. package/dist-client/modeller/figure-properties.js +26 -0
  24. package/dist-client/modeller/figure-properties.js.map +1 -1
  25. package/dist-client/modeller/figure-verdict.d.ts +2 -0
  26. package/dist-client/modeller/figure-verdict.js +10 -2
  27. package/dist-client/modeller/figure-verdict.js.map +1 -1
  28. package/dist-client/modeller/joint-fan.js +4 -7
  29. package/dist-client/modeller/joint-fan.js.map +1 -1
  30. package/dist-client/modeller/links.js +3 -2
  31. package/dist-client/modeller/links.js.map +1 -1
  32. package/dist-client/modeller/product-session.d.ts +68 -112
  33. package/dist-client/route.d.ts +1 -1
  34. package/dist-client/tsconfig.scratch.tsbuildinfo +1 -0
  35. package/dist-client/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +4 -4
  37. package/test/evaluated.test.ts +40 -0
  38. package/test/gizmo.test.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\nexport function boxOf(asset: FigureAsset, part: string): FigureWorldBox | undefined {\n const geometry = compileFigureAsset(asset).evaluate().geometry\n return figureOccupiedBoxesOf(geometry, figureWritersOf(asset)).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(asset: FigureAsset): number {\n const geometry = compileFigureAsset(asset).evaluate().geometry\n const boxes = figureOccupiedBoxesOf(geometry, figureWritersOf(asset))\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(asset: FigureAsset, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(asset, part)\n if (!box) return []\n const value = (id: string) => asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(asset)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(asset: FigureAsset, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[] {\n const box = boxOf(asset, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */\nexport function canMirror(asset: FigureAsset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(asset, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
1
+ {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport { evaluatedGeometryOf } from './evaluated.js'\nimport type { FigureAsset, FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\n/** `values`: the inputs the figure is drawn with, when the box is read as drawn; left out, the asset as it is saved. */\nexport function boxOf(asset: FigureAsset, part: string, values?: Record<string, number>): FigureWorldBox | undefined {\n const geometry = evaluatedGeometryOf(asset, values)\n return figureOccupiedBoxesOf(geometry, figureWritersOf(asset)).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(asset: FigureAsset): number {\n const geometry = evaluatedGeometryOf(asset)\n const boxes = figureOccupiedBoxesOf(geometry, figureWritersOf(asset))\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(asset: FigureAsset, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(asset, part)\n if (!box) return []\n const value = (id: string) => asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(asset)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(asset: FigureAsset, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[] {\n const box = boxOf(asset, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */\nexport function canMirror(asset: FigureAsset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(asset, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
@@ -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
  };
@@ -16,7 +16,6 @@ import './figure-settings.js';
16
16
  import './figure-capability-panel.js';
17
17
  import type { FigureBudgetLine } from './maturity.js';
18
18
  import type { FirstSave } from './first-save.js';
19
- /** Product editor surface. The injected session is shared by page persistence and AI commands. */
20
19
  export declare class FigureEditor extends LitElement {
21
20
  static styles: import("lit").CSSResult[];
22
21
  session?: FigureProductSession;
@@ -200,6 +199,10 @@ export declare class FigureEditor extends LitElement {
200
199
  private renderRepeatNote;
201
200
  private renamePart;
202
201
  /** Size response, fastening and motion for the selected part, as authoring commands. */
202
+ private dimsSaid?;
203
+ private dimsOf;
204
+ private previewWordsSaid?;
205
+ private previewWordsNow;
203
206
  private renderLinks;
204
207
  /**
205
208
  * The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
@@ -265,11 +268,25 @@ export declare class FigureEditor extends LitElement {
265
268
  /** To the release that exists: the history tab's release section, not a second one here. */
266
269
  private goRelease;
267
270
  /** The fastening form's command, seen before it is done; null clears what was shown. */
271
+ /**
272
+ * What a command would do is worked out on a copy of the figure -- several compiles of it -- so it waits for the field to
273
+ * rest: a number typed digit by digit asks once, for the number it ends on. Clearing is at once, and the same question on
274
+ * the same version is not asked again.
275
+ */
276
+ private previewTimer?;
268
277
  private previewCommand;
278
+ private computePreview;
269
279
  /** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
270
280
  private currentPreview;
271
281
  private detachSaid?;
282
+ private detachTimer?;
283
+ private detachAsked?;
284
+ /**
285
+ * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
286
+ * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
287
+ */
272
288
  private detachWords;
289
+ private computeDetachWords;
273
290
  private overhangSaid?;
274
291
  /** Wider than its face: a fact of the relation, said in its details, not as a warning (designer 2026-09-27). */
275
292
  private overhangNote;
@@ -294,6 +311,23 @@ export declare class FigureEditor extends LitElement {
294
311
  };
295
312
  };
296
313
  private choose;
314
+ /**
315
+ * The figure as the session holds it now, parsed once per text: a render asks for it a dozen times, and parsing the whole
316
+ * figure is most of what a selection costs. Read, never changed -- what changes a figure goes through the session.
317
+ */
318
+ /**
319
+ * The session has evaluated this figure at this version, with these inputs: the screen's questions of the text it just
320
+ * parsed are answered from that, not by evaluating the figure again (each is tens of milliseconds on a real figure).
321
+ */
322
+ private seeded;
323
+ private parsed?;
324
+ private parsedAsset;
325
+ private plain?;
326
+ /**
327
+ * The figure as plain data, parsed once per text and frozen: the render, the gizmo's reach and every tool read it, and
328
+ * a copy for each of them cost more than the selection they serve. A change goes through the session, never through this.
329
+ */
330
+ private plainAsset;
297
331
  private asset;
298
332
  /** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
299
333
  private renderTab;
@@ -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 } from '@hatiolab/figure-model';
10
+ import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, parseFigureAsset, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figurePartFaceOf, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf, seedFigureEvaluation, upgradeFigurePlacement } 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';
@@ -39,6 +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
43
  import { jointDragOf } from './joint-drag.js';
43
44
  import { liveOverSession, NOTHING_LIVE, scrubbed, scrubStep } from './live.js';
44
45
  import { startingShapeKindOfBody } from './starting-shape.js';
@@ -55,6 +56,18 @@ function foldSaid() {
55
56
  return false;
56
57
  }
57
58
  /** Product editor surface. The injected session is shared by page persistence and AI commands. */
59
+ /** The words a panel is given when there are none: one array, so it does not read as a change. */
60
+ const NO_WORDS = [];
61
+ /** How long a command's candidate rests before its preview is worked out. */
62
+ 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
+ }
58
71
  let FigureEditor = class FigureEditor extends LitElement {
59
72
  constructor() {
60
73
  super(...arguments);
@@ -497,6 +510,8 @@ let FigureEditor = class FigureEditor extends LitElement {
497
510
  disconnectedCallback() {
498
511
  window.removeEventListener('keydown', this.onKey, true);
499
512
  this.widthWatch.disconnect();
513
+ clearTimeout(this.previewTimer);
514
+ clearTimeout(this.detachTimer);
500
515
  super.disconnectedCallback();
501
516
  }
502
517
  /*
@@ -837,7 +852,7 @@ let FigureEditor = class FigureEditor extends LitElement {
837
852
  */
838
853
  insertRecipe(recipe, values) {
839
854
  try {
840
- const asset = JSON.parse(this.session.source());
855
+ const asset = this.plainAsset();
841
856
  const checked = parseFigureRecipe(recipe);
842
857
  const id = figureRecipeIdFor(asset, checked);
843
858
  const steps = resolveFigureRecipe(checked, { id, ...(checked.target === 'part' ? { target: this.selected } : {}), values });
@@ -854,7 +869,7 @@ let FigureEditor = class FigureEditor extends LitElement {
854
869
  /** The design values of the asset being edited. */
855
870
  designValues() {
856
871
  try {
857
- return this.session ? (JSON.parse(this.session.source()).designInputs ?? {}) : {};
872
+ return this.session ? (this.plainAsset().designInputs ?? {}) : {};
858
873
  }
859
874
  catch {
860
875
  return {};
@@ -867,7 +882,7 @@ let FigureEditor = class FigureEditor extends LitElement {
867
882
  if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id))
868
883
  return none;
869
884
  try {
870
- const asset = JSON.parse(this.session.source());
885
+ const asset = this.plainAsset();
871
886
  const reach = gizmoReachOf(asset, id);
872
887
  /*
873
888
  A size drag shows the face it keeps and the commit keeps the same (figureScaleAnchorOf). Where figure-model cannot
@@ -895,7 +910,7 @@ let FigureEditor = class FigureEditor extends LitElement {
895
910
  transformed(id, result) {
896
911
  const view = this.session.read();
897
912
  /* A part hanging from a joint: the drag edits the joint -- its value, or where it sits (ADR-0093). */
898
- const asset = JSON.parse(this.session.source());
913
+ const asset = this.plainAsset();
899
914
  const drawn = view.geometry.find(g => g.id === id);
900
915
  const drag = drawn ? jointDragOf(asset, ownerOf(id), drawn, result, this.currentInputs()) : null;
901
916
  if (drag) {
@@ -978,7 +993,7 @@ let FigureEditor = class FigureEditor extends LitElement {
978
993
  /** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
979
994
  align(tool) {
980
995
  this.run(() => {
981
- const asset = JSON.parse(this.session.source());
996
+ const asset = this.plainAsset();
982
997
  const changes = alignChanges(asset, this.selected, this.reachOf(this.selected), tool);
983
998
  if (changes.length)
984
999
  this.session.apply(changes);
@@ -993,7 +1008,7 @@ let FigureEditor = class FigureEditor extends LitElement {
993
1008
  const copy = this.session.read().geometry.find(g => !before.has(g.id))?.id;
994
1009
  if (!copy)
995
1010
  return;
996
- this.session.apply(mirrorChanges(JSON.parse(this.session.source()), id, copy, axis));
1011
+ this.session.apply(mirrorChanges(this.plainAsset(), id, copy, axis));
997
1012
  this.selected = copy;
998
1013
  });
999
1014
  }
@@ -1030,7 +1045,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1030
1045
  @click=${() => this.alignToPlane()}>${i18next.t(`figure.button.align-to-${placement}`)}</button>`;
1031
1046
  }
1032
1047
  alignToPlane() {
1033
- const asset = parseFigureAsset(this.session.source());
1048
+ const asset = this.parsedAsset();
1034
1049
  const { changes, contact, refusal } = figurePlaneAlignChanges(asset, this.selected);
1035
1050
  if (refusal) {
1036
1051
  this.error = i18next.t(`figure.text.align-refused-${refusal.toLowerCase().replace(/_/g, '-')}`, { part: partLabelOf(this.selected) });
@@ -1047,7 +1062,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1047
1062
  return null;
1048
1063
  let asset;
1049
1064
  try {
1050
- asset = JSON.parse(this.session.source());
1065
+ asset = this.plainAsset();
1051
1066
  }
1052
1067
  catch {
1053
1068
  return null;
@@ -1113,22 +1128,39 @@ let FigureEditor = class FigureEditor extends LitElement {
1113
1128
  : String(error);
1114
1129
  }
1115
1130
  }
1116
- /** Size response, fastening and motion for the selected part, as authoring commands. */
1117
- renderLinks(unavailable, section) {
1118
- let asset;
1131
+ dimsOf(asset) {
1132
+ const view = this.session?.read();
1133
+ const key = `${view?.context}|${view?.revision}|${this.selected}`;
1134
+ if (this.dimsSaid?.key === key)
1135
+ return this.dimsSaid.dims;
1136
+ let dims;
1119
1137
  try {
1120
- asset = JSON.parse(this.session.source());
1138
+ dims = boxOf(asset, this.selected, this.currentInputs())?.dims;
1121
1139
  }
1122
1140
  catch {
1123
- return null;
1141
+ dims = undefined;
1124
1142
  }
1125
- let dims;
1143
+ this.dimsSaid = { key, dims };
1144
+ return dims;
1145
+ }
1146
+ previewWordsNow() {
1147
+ const preview = this.currentPreview();
1148
+ const language = i18next.language;
1149
+ if (this.previewWordsSaid && this.previewWordsSaid.preview === preview && this.previewWordsSaid.part === this.selected && this.previewWordsSaid.language === language)
1150
+ return this.previewWordsSaid.words;
1151
+ const words = previewWordsOf(preview, this.selected, (key, params) => i18next.t(key, params));
1152
+ this.previewWordsSaid = { preview, part: this.selected, language, words };
1153
+ return words;
1154
+ }
1155
+ renderLinks(unavailable, section) {
1156
+ let asset;
1126
1157
  try {
1127
- dims = boxOf(asset, this.selected)?.dims;
1158
+ asset = this.plainAsset();
1128
1159
  }
1129
1160
  catch {
1130
- dims = undefined;
1161
+ return null;
1131
1162
  }
1163
+ const dims = this.dimsOf(asset);
1132
1164
  return html `<figure-links
1133
1165
  .asset=${asset}
1134
1166
  .section=${section}
@@ -1136,10 +1168,10 @@ let FigureEditor = class FigureEditor extends LitElement {
1136
1168
  .dims=${dims}
1137
1169
  ?locked=${unavailable}
1138
1170
  @command=${(e) => this.linkCommand(e.detail.action)}
1139
- .previewWords=${previewWordsOf(this.currentPreview(), this.selected, (key, params) => i18next.t(key, params))}
1171
+ .previewWords=${this.previewWordsNow()}
1140
1172
  .detachWords=${this.detachWords(asset)}
1141
- .overhangWords=${section === 'shape' ? this.overhangWords(asset) : []}
1142
- .overhangNote=${section === 'shape' ? this.overhangNote(asset) : []}
1173
+ .overhangWords=${section === 'shape' ? this.overhangWords(asset) : NO_WORDS}
1174
+ .overhangNote=${section === 'shape' ? this.overhangNote(asset) : NO_WORDS}
1143
1175
  .motionEnds=${section === 'motion' ? this.motionEnds() : null}
1144
1176
  @go-fasten=${() => this.goFasten()}
1145
1177
  @preview-command=${(e) => this.previewCommand(e.detail)}
@@ -1170,7 +1202,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1170
1202
  renderControls(view, unavailable) {
1171
1203
  let asset;
1172
1204
  try {
1173
- asset = JSON.parse(this.session.source());
1205
+ asset = this.plainAsset();
1174
1206
  }
1175
1207
  catch {
1176
1208
  return null;
@@ -1259,7 +1291,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1259
1291
  /* 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. */
1260
1292
  const part = this.selected;
1261
1293
  const change = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value };
1262
- this.run(() => this.session.apply(field.role !== 'state' && part ? figureSizeEditChanges(parseFigureAsset(this.session.source()), part, [change]) : [change]));
1294
+ this.run(() => this.session.apply(field.role !== 'state' && part ? figureSizeEditChanges(this.parsedAsset(), part, [change]) : [change]));
1263
1295
  // Rejected edits must also restore the displayed numeric value.
1264
1296
  input.value = String(this.session.read().fields.find(i => i.id === field.id).value);
1265
1297
  }
@@ -1597,7 +1629,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1597
1629
  if (!Number.isFinite(pitch) || pitch === field.value)
1598
1630
  return;
1599
1631
  /* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
1600
- const from = this.pitchInputOf(JSON.parse(this.session.source()), this.selected);
1632
+ const from = this.pitchInputOf(this.plainAsset(), this.selected);
1601
1633
  if (from) {
1602
1634
  this.run(() => this.session.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]));
1603
1635
  input.value = String(this.session.read().fields.find(i => i.id === from)?.value ?? field.value);
@@ -1633,20 +1665,29 @@ let FigureEditor = class FigureEditor extends LitElement {
1633
1665
  return i18next.t('figure.text.release-state-released', { version: String(version) });
1634
1666
  return version > 0 ? i18next.t('figure.text.release-state-draft-over', { version: String(version) }) : i18next.t('figure.text.release-state-draft-new');
1635
1667
  }
1636
- /** The fastening form's command, seen before it is done; null clears what was shown. */
1637
1668
  previewCommand(candidate) {
1669
+ clearTimeout(this.previewTimer);
1638
1670
  if (!candidate || !this.session || !this.selected) {
1639
1671
  this.preview = undefined;
1640
1672
  return;
1641
1673
  }
1642
1674
  const view = this.session.read();
1675
+ const same = this.preview && this.preview.key === JSON.stringify(candidate) && this.preview.context === view.context && this.preview.part === this.selected && 'revision' in this.preview && this.preview.revision === view.revision;
1676
+ if (same)
1677
+ return;
1678
+ this.previewTimer = setTimeout(() => this.computePreview(candidate), PREVIEW_SETTLE_MS);
1679
+ }
1680
+ computePreview(candidate) {
1681
+ if (!this.session || !this.selected)
1682
+ return;
1683
+ const view = this.session.read();
1643
1684
  const held = { context: view.context, revision: view.revision, key: JSON.stringify(candidate), part: this.selected };
1644
1685
  try {
1645
1686
  const seen = this.session.previewCommand(candidate.action);
1646
1687
  let face;
1647
1688
  if (candidate.face) {
1648
1689
  try {
1649
- face = figurePartFaceOf(parseFigureAsset(this.session.source()), candidate.face.part, candidate.face.face, this.currentInputs());
1690
+ face = figurePartFaceOf(this.parsedAsset(), candidate.face.part, candidate.face.face, this.currentInputs());
1650
1691
  }
1651
1692
  catch {
1652
1693
  face = undefined;
@@ -1681,14 +1722,32 @@ let FigureEditor = class FigureEditor extends LitElement {
1681
1722
  return undefined;
1682
1723
  return p;
1683
1724
  }
1725
+ /**
1726
+ * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
1727
+ * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
1728
+ */
1684
1729
  detachWords(asset) {
1685
1730
  const view = this.session?.read();
1686
1731
  const joint = asset?.joints?.find(j => j.body1.part === this.selected);
1687
1732
  if (!asset || !view || !joint || !this.session)
1688
- return [];
1733
+ return NO_WORDS;
1689
1734
  const key = `${view.context}|${view.revision}|${this.selected}`;
1690
1735
  if (this.detachSaid?.key === key)
1691
1736
  return this.detachSaid.words;
1737
+ if (this.detachAsked !== key) {
1738
+ this.detachAsked = key;
1739
+ clearTimeout(this.detachTimer);
1740
+ this.detachTimer = setTimeout(() => {
1741
+ const now = this.session?.read();
1742
+ if (!now || `${now.context}|${now.revision}|${this.selected}` !== key)
1743
+ return;
1744
+ this.computeDetachWords(asset, key);
1745
+ this.requestUpdate();
1746
+ }, PREVIEW_SETTLE_MS);
1747
+ }
1748
+ return NO_WORDS;
1749
+ }
1750
+ computeDetachWords(asset, key) {
1692
1751
  const say = (k, params) => i18next.t(k, params);
1693
1752
  let words;
1694
1753
  try {
@@ -1717,7 +1776,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1717
1776
  overhangWords(asset) {
1718
1777
  const view = this.session?.read();
1719
1778
  if (!asset || !view || !this.selected)
1720
- return [];
1779
+ return NO_WORDS;
1721
1780
  const key = `${view.context}|${view.revision}|${this.selected}`;
1722
1781
  if (this.overhangSaid?.key === key)
1723
1782
  return this.overhangSaid.words;
@@ -1759,7 +1818,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1759
1818
  if (this.motionEndsSaid?.key !== key) {
1760
1819
  let ends = null;
1761
1820
  try {
1762
- ends = motionEndsOf(parseFigureAsset(this.session.source()), this.selected, this.currentInputs());
1821
+ ends = motionEndsOf(this.parsedAsset(), this.selected, this.currentInputs());
1763
1822
  }
1764
1823
  catch {
1765
1824
  ends = null;
@@ -1803,9 +1862,46 @@ let FigureEditor = class FigureEditor extends LitElement {
1803
1862
  if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
1804
1863
  this.tab = 'properties';
1805
1864
  }
1865
+ /**
1866
+ * The figure as the session holds it now, parsed once per text: a render asks for it a dozen times, and parsing the whole
1867
+ * figure is most of what a selection costs. Read, never changed -- what changes a figure goes through the session.
1868
+ */
1869
+ /**
1870
+ * The session has evaluated this figure at this version, with these inputs: the screen's questions of the text it just
1871
+ * parsed are answered from that, not by evaluating the figure again (each is tens of milliseconds on a real figure).
1872
+ */
1873
+ seeded(asset) {
1874
+ const view = this.session?.read();
1875
+ if (view?.geometry.length) {
1876
+ const values = this.currentInputs();
1877
+ seedEvaluatedGeometry(asset, values, view.geometry);
1878
+ seedFigureEvaluation(asset, values, view.geometry);
1879
+ }
1880
+ return asset;
1881
+ }
1882
+ parsedAsset() {
1883
+ const text = this.session.source();
1884
+ if (this.parsed?.text !== text) {
1885
+ /* The session's text is what its workspace exported, compiled then: it is read here, not compiled again. */
1886
+ const plain = this.plainAsset();
1887
+ const upgraded = upgradeFigurePlacement(plain);
1888
+ this.parsed = { text, asset: upgraded === plain ? plain : this.seeded(deepFreeze(upgraded)) };
1889
+ }
1890
+ return this.parsed.asset;
1891
+ }
1892
+ /**
1893
+ * The figure as plain data, parsed once per text and frozen: the render, the gizmo's reach and every tool read it, and
1894
+ * a copy for each of them cost more than the selection they serve. A change goes through the session, never through this.
1895
+ */
1896
+ plainAsset() {
1897
+ const text = this.session.source();
1898
+ if (this.plain?.text !== text)
1899
+ this.plain = { text, asset: this.seeded(deepFreeze(JSON.parse(text))) };
1900
+ return this.plain.asset;
1901
+ }
1806
1902
  asset() {
1807
1903
  try {
1808
- return JSON.parse(this.session.source());
1904
+ return this.plainAsset();
1809
1905
  }
1810
1906
  catch {
1811
1907
  return undefined;
@@ -1939,7 +2035,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1939
2035
  let summary;
1940
2036
  try {
1941
2037
  summary = this.selected
1942
- ? partSummaryOf(parseFigureAsset(this.session.source()), ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
2038
+ ? partSummaryOf(this.parsedAsset(), ownerOf(this.selected), (key, params) => i18next.t(key, params), i18next.language || 'ko')
1943
2039
  : undefined;
1944
2040
  }
1945
2041
  catch {
@@ -1979,7 +2075,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1979
2075
  */
1980
2076
  let roomOnly = [];
1981
2077
  try {
1982
- roomOnly = this.selected ? figureRoomOnlyInputsOf(parseFigureAsset(this.session.source()), this.selected) : [];
2078
+ roomOnly = this.selected ? figureRoomOnlyInputsOf(this.parsedAsset(), this.selected) : [];
1983
2079
  }
1984
2080
  catch {
1985
2081
  roomOnly = [];
@@ -1990,7 +2086,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1990
2086
  try {
1991
2087
  const body = view.geometry.find(g => g.id === this.selected);
1992
2088
  const placed = body ? placementOf(body) : undefined;
1993
- const parsed = this.selected ? parseFigureAsset(this.session.source()) : undefined;
2089
+ const parsed = this.selected ? this.parsedAsset() : undefined;
1994
2090
  if (placed && parsed)
1995
2091
  held = {
1996
2092
  position: placed.position,
@@ -2285,7 +2381,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2285
2381
  async askRoomRefit() {
2286
2382
  let refit;
2287
2383
  try {
2288
- refit = figureRoomRefitOf(parseFigureAsset(this.session.source()));
2384
+ refit = figureRoomRefitOf(this.parsedAsset());
2289
2385
  }
2290
2386
  catch (error) {
2291
2387
  this.error = this.said(error);