@things-factory/figure-ui 10.1.79 → 10.1.82

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 (145) hide show
  1. package/client/figure-assistant.ts +2 -2
  2. package/client/graphql/index.ts +99 -10
  3. package/client/graphql/reached.ts +25 -0
  4. package/client/graphql/store.ts +27 -4
  5. package/client/modeller/ai-proposal.ts +74 -9
  6. package/client/modeller/align.ts +2 -3
  7. package/client/modeller/canvas.ts +18 -5
  8. package/client/modeller/command-preview.ts +113 -0
  9. package/client/modeller/editor.ts +826 -54
  10. package/client/modeller/fields.ts +12 -4
  11. package/client/modeller/figure-facts.ts +2 -2
  12. package/client/modeller/figure-first-save.ts +36 -14
  13. package/client/modeller/figure-history-panel.ts +133 -14
  14. package/client/modeller/figure-links.ts +385 -68
  15. package/client/modeller/figure-parts.ts +56 -2
  16. package/client/modeller/figure-preview.ts +19 -1
  17. package/client/modeller/figure-properties.ts +3 -3
  18. package/client/modeller/figure-revert-choice.ts +49 -0
  19. package/client/modeller/first-save.ts +16 -0
  20. package/client/modeller/gizmo.ts +0 -16
  21. package/client/modeller/links.ts +142 -15
  22. package/client/modeller/motion-ends.ts +83 -0
  23. package/client/modeller/occupancy.ts +28 -2
  24. package/client/modeller/product-session.ts +154 -18
  25. package/client/modeller/refusals.ts +15 -0
  26. package/client/modeller/revert-request.ts +26 -0
  27. package/client/modeller/side-panel-styles.ts +12 -0
  28. package/client/modeller/stage-model.ts +25 -3
  29. package/client/pages/figure-modeller-page.ts +68 -29
  30. package/dist-client/figure-assistant.js +2 -2
  31. package/dist-client/figure-assistant.js.map +1 -1
  32. package/dist-client/graphql/index.d.ts +39 -2
  33. package/dist-client/graphql/index.js +63 -9
  34. package/dist-client/graphql/index.js.map +1 -1
  35. package/dist-client/graphql/reached.d.ts +10 -0
  36. package/dist-client/graphql/reached.js +28 -0
  37. package/dist-client/graphql/reached.js.map +1 -0
  38. package/dist-client/graphql/store.js +27 -4
  39. package/dist-client/graphql/store.js.map +1 -1
  40. package/dist-client/modeller/ai-proposal.d.ts +31 -2
  41. package/dist-client/modeller/ai-proposal.js +65 -6
  42. package/dist-client/modeller/ai-proposal.js.map +1 -1
  43. package/dist-client/modeller/align.d.ts +1 -1
  44. package/dist-client/modeller/align.js +0 -2
  45. package/dist-client/modeller/align.js.map +1 -1
  46. package/dist-client/modeller/canvas.d.ts +11 -0
  47. package/dist-client/modeller/canvas.js +33 -4
  48. package/dist-client/modeller/canvas.js.map +1 -1
  49. package/dist-client/modeller/clips.d.ts +1 -1
  50. package/dist-client/modeller/command-preview.d.ts +35 -0
  51. package/dist-client/modeller/command-preview.js +95 -0
  52. package/dist-client/modeller/command-preview.js.map +1 -0
  53. package/dist-client/modeller/editor.d.ts +113 -1
  54. package/dist-client/modeller/editor.js +850 -50
  55. package/dist-client/modeller/editor.js.map +1 -1
  56. package/dist-client/modeller/fields.d.ts +3 -2
  57. package/dist-client/modeller/fields.js +10 -3
  58. package/dist-client/modeller/fields.js.map +1 -1
  59. package/dist-client/modeller/figure-facts.js +2 -2
  60. package/dist-client/modeller/figure-facts.js.map +1 -1
  61. package/dist-client/modeller/figure-first-save.d.ts +3 -4
  62. package/dist-client/modeller/figure-first-save.js +41 -14
  63. package/dist-client/modeller/figure-first-save.js.map +1 -1
  64. package/dist-client/modeller/figure-history-panel.d.ts +31 -0
  65. package/dist-client/modeller/figure-history-panel.js +130 -14
  66. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-links.d.ts +51 -1
  68. package/dist-client/modeller/figure-links.js +392 -72
  69. package/dist-client/modeller/figure-links.js.map +1 -1
  70. package/dist-client/modeller/figure-parts.d.ts +7 -0
  71. package/dist-client/modeller/figure-parts.js +55 -2
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.d.ts +6 -0
  74. package/dist-client/modeller/figure-preview.js +22 -1
  75. package/dist-client/modeller/figure-preview.js.map +1 -1
  76. package/dist-client/modeller/figure-properties.js +3 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-revert-choice.d.ts +17 -0
  79. package/dist-client/modeller/figure-revert-choice.js +59 -0
  80. package/dist-client/modeller/figure-revert-choice.js.map +1 -0
  81. package/dist-client/modeller/first-save.d.ts +7 -0
  82. package/dist-client/modeller/first-save.js +17 -0
  83. package/dist-client/modeller/first-save.js.map +1 -1
  84. package/dist-client/modeller/gizmo.d.ts +0 -6
  85. package/dist-client/modeller/gizmo.js +0 -18
  86. package/dist-client/modeller/gizmo.js.map +1 -1
  87. package/dist-client/modeller/links.d.ts +49 -4
  88. package/dist-client/modeller/links.js +141 -15
  89. package/dist-client/modeller/links.js.map +1 -1
  90. package/dist-client/modeller/motion-ends.d.ts +12 -0
  91. package/dist-client/modeller/motion-ends.js +65 -0
  92. package/dist-client/modeller/motion-ends.js.map +1 -0
  93. package/dist-client/modeller/occupancy.d.ts +14 -1
  94. package/dist-client/modeller/occupancy.js +14 -1
  95. package/dist-client/modeller/occupancy.js.map +1 -1
  96. package/dist-client/modeller/product-session.d.ts +43 -2
  97. package/dist-client/modeller/product-session.js +146 -12
  98. package/dist-client/modeller/product-session.js.map +1 -1
  99. package/dist-client/modeller/refusals.d.ts +13 -0
  100. package/dist-client/modeller/refusals.js +16 -0
  101. package/dist-client/modeller/refusals.js.map +1 -1
  102. package/dist-client/modeller/revert-request.d.ts +16 -0
  103. package/dist-client/modeller/revert-request.js +11 -0
  104. package/dist-client/modeller/revert-request.js.map +1 -0
  105. package/dist-client/modeller/side-panel-styles.js +12 -0
  106. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  107. package/dist-client/modeller/stage-model.d.ts +18 -2
  108. package/dist-client/modeller/stage-model.js +21 -3
  109. package/dist-client/modeller/stage-model.js.map +1 -1
  110. package/dist-client/pages/figure-modeller-page.d.ts +8 -1
  111. package/dist-client/pages/figure-modeller-page.js +72 -34
  112. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  113. package/dist-client/route.d.ts +1 -1
  114. package/dist-client/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +6 -6
  116. package/test/ai-proposal.test.ts +2 -2
  117. package/test/align.test.ts +50 -5
  118. package/test/browser/acceptance-page.js +187 -0
  119. package/test/browser/build.mjs +5 -1
  120. package/test/browser/revert-page.js +47 -0
  121. package/test/browser/serve-acceptance.mjs +85 -0
  122. package/test/browser/serve-revert.mjs +119 -0
  123. package/test/clip-ai-proposal.test.ts +104 -0
  124. package/test/clip-consent.test.ts +88 -0
  125. package/test/command-preview.test.ts +288 -0
  126. package/test/fields.test.ts +4 -3
  127. package/test/figure-assistant-surface.test.ts +4 -1
  128. package/test/first-minute.test.ts +96 -0
  129. package/test/first-save-suggest.test.ts +42 -0
  130. package/test/gizmo.test.ts +20 -16
  131. package/test/joint-fan.test.ts +2 -1
  132. package/test/layout.test.ts +9 -2
  133. package/test/links.test.ts +7 -4
  134. package/test/proposal-record.test.ts +126 -0
  135. package/test/release-entry.test.ts +28 -0
  136. package/test/release-panel.test.ts +3 -2
  137. package/test/resize-with-placement.test.ts +225 -0
  138. package/test/revert-choice.test.ts +39 -0
  139. package/test/revert-keeping.test.ts +102 -0
  140. package/test/save-landed.test.ts +198 -0
  141. package/translations/en.json +222 -10
  142. package/translations/ja.json +223 -11
  143. package/translations/ko.json +223 -11
  144. package/translations/ms.json +222 -10
  145. package/translations/zh.json +223 -11
@@ -2,10 +2,13 @@ import { LitElement, type PropertyValues } from 'lit';
2
2
  import type { GizmoReach } from './gizmo.js';
3
3
  import type { FigureRenderPart } from './renderer.js';
4
4
  import { type ViewSettings } from './figure-view.js';
5
+ import { type ScaleAnchor } from './stage-model.js';
5
6
  export declare class FigureCanvas extends LitElement {
6
7
  static styles: import("lit").CSSResult;
7
8
  parts: FigureRenderPart[];
8
9
  palette: Record<string, string>;
10
+ /** The face a size drag keeps, per part (figure-model's `figureScaleAnchorOf`), so the drag shows what is written. */
11
+ anchors: Record<string, ScaleAnchor>;
9
12
  /** Which figure is open. Another one stands the board up again. */
10
13
  context: string;
11
14
  selected: string;
@@ -18,8 +21,16 @@ export declare class FigureCanvas extends LitElement {
18
21
  * would reach the editor as an edit.
19
22
  */
20
23
  viewOnly: boolean;
24
+ /** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */
25
+ startShown: boolean;
21
26
  /** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */
22
27
  fan: FigureRenderPart | null;
28
+ /**
29
+ * A command seen before it is done (ADR-0097 §5.5): the bodies it would move, where they would stand, translucent,
30
+ * and the face they would be fastened to, lit. Shown, never picked, never saved; gone when the command is.
31
+ */
32
+ ghost: FigureRenderPart[];
33
+ highlight: FigureRenderPart | null;
23
34
  private stage;
24
35
  private scene?;
25
36
  private board?;
@@ -6,6 +6,7 @@ import { create, Model, niceGridStep } from '@hatiolab/things-scene';
6
6
  import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js';
7
7
  import { boardFor, changedKeys, componentOf, gizmoResultOf } from './stage-model.js';
8
8
  import { JOINT_FAN } from './joint-fan.js';
9
+ import { PREVIEW, TARGET_FACE } from './command-preview.js';
9
10
  import { ownerOf } from './fields.js';
10
11
  /* The fan's colour when the figure's palette names no accent. */
11
12
  const FAN_COLOUR = '#ff8a00';
@@ -16,6 +17,8 @@ let FigureCanvas = class FigureCanvas extends LitElement {
16
17
  super(...arguments);
17
18
  this.parts = [];
18
19
  this.palette = {};
20
+ /** The face a size drag keeps, per part (figure-model's `figureScaleAnchorOf`), so the drag shows what is written. */
21
+ this.anchors = {};
19
22
  /** Which figure is open. Another one stands the board up again. */
20
23
  this.context = '';
21
24
  this.selected = '';
@@ -28,8 +31,16 @@ let FigureCanvas = class FigureCanvas extends LitElement {
28
31
  * would reach the editor as an edit.
29
32
  */
30
33
  this.viewOnly = false;
34
+ /** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */
35
+ this.startShown = false;
31
36
  /** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */
32
37
  this.fan = null;
38
+ /**
39
+ * A command seen before it is done (ADR-0097 §5.5): the bodies it would move, where they would stand, translucent,
40
+ * and the face they would be fastened to, lit. Shown, never picked, never saved; gone when the command is.
41
+ */
42
+ this.ghost = [];
43
+ this.highlight = null;
33
44
  /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
34
45
  this.writing = false;
35
46
  /** Components the gizmo changed since the last commit, by part name. */
@@ -71,7 +82,7 @@ let FigureCanvas = class FigureCanvas extends LitElement {
71
82
  }
72
83
  `; }
73
84
  render() {
74
- return html `<div stage></div>${this.parts.length ? nothing : html `<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`;
85
+ return html `<div stage></div>${this.parts.length || this.startShown ? nothing : html `<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`;
75
86
  }
76
87
  firstUpdated() {
77
88
  this.standUp();
@@ -81,7 +92,7 @@ let FigureCanvas = class FigureCanvas extends LitElement {
81
92
  updated(changed) {
82
93
  if (!this.hasUpdated)
83
94
  return;
84
- if (changed.has('parts') || changed.has('palette') || changed.has('context') || changed.has('fan'))
95
+ if (['parts', 'palette', 'context', 'fan', 'anchors', 'ghost', 'highlight'].some(key => changed.has(key)))
85
96
  this.standUp();
86
97
  if (changed.has('view'))
87
98
  this.applyView();
@@ -177,10 +188,12 @@ let FigureCanvas = class FigureCanvas extends LitElement {
177
188
  }
178
189
  }
179
190
  components() {
180
- const parts = this.parts.map(part => componentOf(part, this.palette, this.board)).filter((c) => !!c);
191
+ const parts = this.parts.map(part => componentOf(part, this.palette, this.board, this.anchors[ownerOf(part.id)])).filter((c) => !!c);
181
192
  /* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
182
193
  const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board) : null;
183
- return fan ? [...parts, { ...fan, capturable: false, locked: true }] : parts;
194
+ const accent = this.palette['palette.accent'] ?? FAN_COLOUR;
195
+ const shown = [...(fan ? [fan] : []), ...this.ghost.map(part => componentOf(part, { ...this.palette, [PREVIEW]: accent, [TARGET_FACE]: accent }, this.board)), ...(this.highlight ? [componentOf(this.highlight, { ...this.palette, [TARGET_FACE]: accent }, this.board)] : [])];
196
+ return [...parts, ...shown.filter((c) => !!c).map(c => ({ ...c, capturable: false, locked: true }))];
184
197
  }
185
198
  /** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */
186
199
  standing() {
@@ -344,6 +357,10 @@ __decorate([
344
357
  property({ attribute: false }),
345
358
  __metadata("design:type", Object)
346
359
  ], FigureCanvas.prototype, "palette", void 0);
360
+ __decorate([
361
+ property({ attribute: false }),
362
+ __metadata("design:type", Object)
363
+ ], FigureCanvas.prototype, "anchors", void 0);
347
364
  __decorate([
348
365
  property(),
349
366
  __metadata("design:type", Object)
@@ -364,10 +381,22 @@ __decorate([
364
381
  property({ type: Boolean }),
365
382
  __metadata("design:type", Object)
366
383
  ], FigureCanvas.prototype, "viewOnly", void 0);
384
+ __decorate([
385
+ property({ type: Boolean }),
386
+ __metadata("design:type", Object)
387
+ ], FigureCanvas.prototype, "startShown", void 0);
367
388
  __decorate([
368
389
  property({ attribute: false }),
369
390
  __metadata("design:type", Object)
370
391
  ], FigureCanvas.prototype, "fan", void 0);
392
+ __decorate([
393
+ property({ attribute: false }),
394
+ __metadata("design:type", Array)
395
+ ], FigureCanvas.prototype, "ghost", void 0);
396
+ __decorate([
397
+ property({ attribute: false }),
398
+ __metadata("design:type", Object)
399
+ ], FigureCanvas.prototype, "highlight", void 0);
371
400
  __decorate([
372
401
  query('div[stage]'),
373
402
  __metadata("design:type", HTMLDivElement)
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../client/modeller/canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { create, Model, niceGridStep } from '@hatiolab/things-scene'\n\nimport type { GizmoReach } from './gizmo.js'\nimport type { FigureRenderPart } from './renderer.js'\nimport { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'\nimport { boardFor, changedKeys, componentOf, gizmoResultOf, type Board } from './stage-model.js'\nimport { JOINT_FAN } from './joint-fan.js'\nimport { ownerOf } from './fields.js'\n\n/*\n * The modeller's canvas -- a things-scene board in editing mode.\n *\n * Each body figure-model evaluates stands on the board as a `figure-part` component, so everything the canvas\n * has comes from the scene, not from code of our own: picking and the orange outline, the gizmo with its snapping and\n * live readout, box and shift selection, the camera (damping, zoom toward the cursor, double-click to fit, the six\n * view presets and their keys), the studio environment, the coordinate ground, lights and shadows, supersampling.\n * An earlier canvas drew the bodies with three.js directly and lost all of that (user, 2026-09-24: \"why nerf it?\").\n *\n * What the gizmo writes back to a component comes out as `part-transformed` in the inputs' terms (`stage-model`),\n * and the editor turns it into one step of the history; the next evaluation puts the component where the inputs say,\n * so a change a part cannot take (a fastened part moved) springs back.\n */\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown>) => void\n dispose?: () => void\n}\ninterface SceneLayer extends SceneComponent {\n components?: SceneComponent[]\n root?: { on?: (event: string, handler: (...args: unknown[]) => void) => void; selected?: SceneComponent[] }\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n addComponent?: (component: SceneComponent) => void\n removeComponent?: (component: SceneComponent) => void\n invalidate?: () => void\n fitCameraToModel?: () => void\n setGizmoMode?: (mode: 'translate' | 'rotate' | 'scale') => void\n app?: unknown\n}\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: SceneLayer\n}\n\n/* The fan's colour when the figure's palette names no accent. */\nconst FAN_COLOUR = '#ff8a00'\n\n/* The 2D side of the board, which the author never sees on this page but the scene paints. */\nconst PAPER = '#ffffff'\n\n@customElement('figure-canvas')\nexport class FigureCanvas extends LitElement {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n min-height: 0;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n /* The scene sets position: relative inline on its target; the size is given so it still fills the host. */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n margin: 0;\n text-align: center;\n pointer-events: none;\n color: #f4f4f4;\n }\n `\n @property({ attribute: false }) parts: FigureRenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n /** Which figure is open. Another one stands the board up again. */\n @property() context = ''\n @property() selected = ''\n /** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */\n @property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }\n @property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW\n /**\n * View only: nothing that happens on the board is told out. The card's picture is taken from a second canvas stood up\n * off screen (`thumbnail.ts`); the scene settles its components as it builds them, and a change told from there\n * would reach the editor as an edit.\n */\n @property({ type: Boolean }) viewOnly = false\n /** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */\n @property({ attribute: false }) fan: FigureRenderPart | null = null\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n private scene?: SceneHandle\n private board?: Board & { depth: number }\n private standingFor?: string\n private watcher?: ResizeObserver\n /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */\n private writing = false\n\n render() {\n return html`<div stage></div>${this.parts.length ? nothing : html`<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n protected updated(changed: PropertyValues) {\n if (!this.hasUpdated) return\n if (changed.has('parts') || changed.has('palette') || changed.has('context') || changed.has('fan')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('selected')) this.showSelected()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * The board as a picture, for the card: the 3D view at this size, or nothing when it cannot be taken. The scene's\n * `toDataURL` renders the 2D board (a white sheet), and the 3D renderer keeps no drawing buffer, so the\n * scene's own `snapshot3d` draws and reads in one task. A picture that cannot be taken is not an error and is not\n * faked (user, 2026-09-24: no card had a picture after this canvas moved onto the scene).\n */\n async snapshot(width = 480, height = 360): Promise<string | undefined> {\n const layer = this.scene?.root as (SceneLayer & { snapshot3d?: (w?: number, h?: number, type?: string, quality?: number) => Promise<string | undefined> }) | undefined\n if (!layer?.snapshot3d) return undefined\n try {\n /* webp: a card needs no alpha, and png is three or four times heavier in every list response. */\n const url = await layer.snapshot3d(width, height, 'image/webp', 0.85)\n return typeof url === 'string' && url.startsWith('data:') ? url : undefined\n } catch (e) {\n console.warn(`[thumbnail] could not take the picture -- ${(e as Error).message}`)\n return undefined\n }\n }\n\n /** The whole figure in view: the board's fit, then the 3D camera's own. */\n fit() {\n this.scene?.fit?.('ratio')\n this.scene?.root?.fitCameraToModel?.()\n }\n\n /** The gizmo in a mode on the selected part, as W, E and R do with the canvas focused -- the scene's toggle. */\n gizmo(mode: 'translate' | 'rotate' | 'scale') {\n this.scene?.root?.setGizmoMode?.(mode)\n }\n\n /**\n * Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or\n * one that has outgrown it -- standing up again loses the selection and the camera.\n */\n private standUp() {\n if (!this.stage) return\n const needed = boardFor(this.parts, this.board)\n const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth\n if (this.scene && this.standingFor === this.context) {\n /*\n A part dragged past the board's edge grows the board where it stands. Standing it up again lost the selection,\n the gizmo and the camera after every such drag -- the gizmo seemed not to work at all (user, 2026-09-24).\n */\n if (outgrown) this.grow(needed)\n this.sync()\n return\n }\n this.teardown()\n if (!this.parts.length) return\n this.board = needed\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: needed.width,\n height: needed.height,\n depth: needed.depth,\n threed: true,\n /* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large. */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n ...sceneLook(this.view ?? DEFAULT_VIEW),\n gridStep: niceGridStep(Math.max(needed.width, needed.height)),\n sky: 'color',\n fillStyle: PAPER,\n components: this.components()\n },\n /* Editing mode: the camera view buttons, gizmo modes, help and keys. */\n mode: 1\n }) as SceneHandle\n this.standingFor = this.context\n this.watchScene()\n this.scene.resize?.()\n this.fit()\n this.showSelected()\n } catch (error) {\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n\n private components(): Record<string, unknown>[] {\n const parts = this.parts.map(part => componentOf(part, this.palette, this.board!)).filter((c): c is Record<string, unknown> => !!c)\n /* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */\n const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null\n return fan ? [...parts, { ...fan, capturable: false, locked: true }] : parts\n }\n\n /** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */\n private standing(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /** A larger board under the same scene: its size, and the grid that suits it. Components are re-placed by `sync`. */\n private grow(needed: Board & { depth: number }) {\n const layer = this.scene?.root\n this.board = needed\n if (!layer) return\n this.writing = true\n try {\n layer.set({ width: needed.width, height: needed.height, depth: needed.depth, gridStep: niceGridStep(Math.max(needed.width, needed.height)) })\n } finally {\n this.writing = false\n }\n }\n\n /** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */\n private sync() {\n const layer = this.scene?.root\n if (!layer || !this.board) return\n const wanted = new Map(this.components().map(c => [c.name as string, c]))\n const standing = this.standing()\n this.writing = true\n try {\n for (const [name, component] of standing) {\n if (wanted.has(name)) continue\n layer.removeComponent?.(component)\n component.dispose?.()\n }\n for (const [name, state] of wanted) {\n const found = standing.get(name)\n if (found) {\n const diff = changedKeys(found.state, state)\n if (diff) found.set(diff)\n continue\n }\n const made = Model.compile({ ...state }, layer.app as never) as SceneComponent | null\n if (made) layer.addComponent?.(made)\n }\n } finally {\n this.writing = false\n }\n layer.invalidate?.()\n }\n\n /** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */\n private watchScene() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root?.on || this.viewOnly) return\n root.on('selected', (after: unknown) => {\n const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name\n const id = typeof name === 'string' ? name : ''\n if (ownerOf(id) !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))\n })\n /*\n A change comes up to the board with the component in the last argument's `origin` (the scene appends\n { origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing.\n */\n /*\n A drag is written once, when it ends. The scene's gizmo writes the component on every move -- a size anchored on\n the opposite face, from the scale it captured when the drag began -- and writing each move back through the\n editor (and the evaluation back onto the component) fought it: the scale gizmo did not work (user, 2026-09-24).\n While a pointer is down the changes are only kept; on release the last one is told, and the board is brought to\n the evaluation. A change with no pointer down (the arrow keys) is told after a short pause.\n */\n layer.on?.('change', (...args: unknown[]) => {\n if (this.writing) return\n const origin = (args[args.length - 1] as { origin?: SceneComponent } | undefined)?.origin\n const name = origin?.state?.name\n if (typeof name !== 'string' || !this.parts.some(p => p.id === name)) return\n this.pending.set(name, origin!)\n if (!this.pointerDown) this.commitSoon(80)\n })\n this.stage.addEventListener('pointerdown', this.onPointerDown, true)\n window.addEventListener('pointerup', this.onPointerUp, true)\n window.addEventListener('pointercancel', this.onPointerUp, true)\n }\n\n /** Components the gizmo changed since the last commit, by part name. */\n private pending = new Map<string, SceneComponent>()\n private pointerDown = false\n private commitTimer?: ReturnType<typeof setTimeout>\n private onPointerDown = () => {\n this.pointerDown = true\n }\n private onPointerUp = () => {\n if (!this.pointerDown) return\n this.pointerDown = false\n /* After the scene's own drag end (it settles the depth and rebuilds the geometry on release). */\n this.commitSoon(0)\n }\n private commitSoon(delay: number) {\n clearTimeout(this.commitTimer)\n this.commitTimer = setTimeout(() => this.commit(), delay)\n }\n /** Tell each changed part once, in the inputs' terms; then put the board where the inputs say (a refusal springs back). */\n private commit() {\n /*\n Not while a pointer is down: a pause set by a change just before the press (the arrow keys, the scene settling\n the pick) fired in the middle of the drag and wrote where the part was then -- a drag on a part placed by a rule\n became two steps, the first one a few millimetres in (browser check, 2026-09-25). What is pending waits for the release.\n */\n if (this.pointerDown || !this.board || !this.pending.size) return\n const changed = [...this.pending]\n this.pending.clear()\n for (const [name, component] of changed) {\n const part = this.parts.find(p => p.id === name)\n if (!part) continue\n const result = gizmoResultOf(part, component.state, this.board)\n if (!result.position && !result.rotation && !result.scale) continue\n this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }))\n }\n setTimeout(() => this.sync())\n }\n\n private showSelected() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root) return\n /* A repeat is selected whole: every copy of it (the editor selects the part the author made). */\n const wanted = this.selected ? [...this.standing()].filter(([name]) => ownerOf(name) === this.selected).map(([, component]) => component) : []\n const now = root.selected ?? []\n if (now.length === wanted.length && wanted.every((one, i) => now[i] === one)) return\n root.selected = wanted\n layer.invalidate?.()\n }\n\n /** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */\n private applyView() {\n const layer = this.scene?.root\n if (!layer) return\n layer.set(sceneLook(this.view ?? DEFAULT_VIEW))\n layer.invalidate?.()\n }\n\n private teardown() {\n clearTimeout(this.commitTimer)\n this.pending.clear()\n this.stage?.removeEventListener('pointerdown', this.onPointerDown, true)\n window.removeEventListener('pointerup', this.onPointerUp, true)\n window.removeEventListener('pointercancel', this.onPointerUp, true)\n this.scene?.dispose?.()\n this.scene = undefined\n this.standingFor = undefined\n this.board = undefined\n }\n}\n"]}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../client/modeller/canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { create, Model, niceGridStep } from '@hatiolab/things-scene'\n\nimport type { GizmoReach } from './gizmo.js'\nimport type { FigureRenderPart } from './renderer.js'\nimport { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'\nimport { boardFor, changedKeys, componentOf, gizmoResultOf, type Board, type ScaleAnchor } from './stage-model.js'\nimport { JOINT_FAN } from './joint-fan.js'\nimport { PREVIEW, TARGET_FACE } from './command-preview.js'\nimport { ownerOf } from './fields.js'\n\n/*\n * The modeller's canvas -- a things-scene board in editing mode.\n *\n * Each body figure-model evaluates stands on the board as a `figure-part` component, so everything the canvas\n * has comes from the scene, not from code of our own: picking and the orange outline, the gizmo with its snapping and\n * live readout, box and shift selection, the camera (damping, zoom toward the cursor, double-click to fit, the six\n * view presets and their keys), the studio environment, the coordinate ground, lights and shadows, supersampling.\n * An earlier canvas drew the bodies with three.js directly and lost all of that (user, 2026-09-24: \"why nerf it?\").\n *\n * What the gizmo writes back to a component comes out as `part-transformed` in the inputs' terms (`stage-model`),\n * and the editor turns it into one step of the history; the next evaluation puts the component where the inputs say,\n * so a change a part cannot take (a fastened part moved) springs back.\n */\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown>) => void\n dispose?: () => void\n}\ninterface SceneLayer extends SceneComponent {\n components?: SceneComponent[]\n root?: { on?: (event: string, handler: (...args: unknown[]) => void) => void; selected?: SceneComponent[] }\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n addComponent?: (component: SceneComponent) => void\n removeComponent?: (component: SceneComponent) => void\n invalidate?: () => void\n fitCameraToModel?: () => void\n setGizmoMode?: (mode: 'translate' | 'rotate' | 'scale') => void\n app?: unknown\n}\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: SceneLayer\n}\n\n/* The fan's colour when the figure's palette names no accent. */\nconst FAN_COLOUR = '#ff8a00'\n\n/* The 2D side of the board, which the author never sees on this page but the scene paints. */\nconst PAPER = '#ffffff'\n\n@customElement('figure-canvas')\nexport class FigureCanvas extends LitElement {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n min-height: 0;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n /* The scene sets position: relative inline on its target; the size is given so it still fills the host. */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n margin: 0;\n text-align: center;\n pointer-events: none;\n color: #f4f4f4;\n }\n `\n @property({ attribute: false }) parts: FigureRenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n /** The face a size drag keeps, per part (figure-model's `figureScaleAnchorOf`), so the drag shows what is written. */\n @property({ attribute: false }) anchors: Record<string, ScaleAnchor> = {}\n /** Which figure is open. Another one stands the board up again. */\n @property() context = ''\n @property() selected = ''\n /** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */\n @property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }\n @property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW\n /**\n * View only: nothing that happens on the board is told out. The card's picture is taken from a second canvas stood up\n * off screen (`thumbnail.ts`); the scene settles its components as it builds them, and a change told from there\n * would reach the editor as an edit.\n */\n @property({ type: Boolean }) viewOnly = false\n /** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */\n @property({ type: Boolean }) startShown = false\n /** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */\n @property({ attribute: false }) fan: FigureRenderPart | null = null\n /**\n * A command seen before it is done (ADR-0097 §5.5): the bodies it would move, where they would stand, translucent,\n * and the face they would be fastened to, lit. Shown, never picked, never saved; gone when the command is.\n */\n @property({ attribute: false }) ghost: FigureRenderPart[] = []\n @property({ attribute: false }) highlight: FigureRenderPart | null = null\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n private scene?: SceneHandle\n private board?: Board & { depth: number }\n private standingFor?: string\n private watcher?: ResizeObserver\n /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */\n private writing = false\n\n render() {\n return html`<div stage></div>${this.parts.length || this.startShown ? nothing : html`<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n protected updated(changed: PropertyValues) {\n if (!this.hasUpdated) return\n if (['parts', 'palette', 'context', 'fan', 'anchors', 'ghost', 'highlight'].some(key => changed.has(key))) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('selected')) this.showSelected()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * The board as a picture, for the card: the 3D view at this size, or nothing when it cannot be taken. The scene's\n * `toDataURL` renders the 2D board (a white sheet), and the 3D renderer keeps no drawing buffer, so the\n * scene's own `snapshot3d` draws and reads in one task. A picture that cannot be taken is not an error and is not\n * faked (user, 2026-09-24: no card had a picture after this canvas moved onto the scene).\n */\n async snapshot(width = 480, height = 360): Promise<string | undefined> {\n const layer = this.scene?.root as (SceneLayer & { snapshot3d?: (w?: number, h?: number, type?: string, quality?: number) => Promise<string | undefined> }) | undefined\n if (!layer?.snapshot3d) return undefined\n try {\n /* webp: a card needs no alpha, and png is three or four times heavier in every list response. */\n const url = await layer.snapshot3d(width, height, 'image/webp', 0.85)\n return typeof url === 'string' && url.startsWith('data:') ? url : undefined\n } catch (e) {\n console.warn(`[thumbnail] could not take the picture -- ${(e as Error).message}`)\n return undefined\n }\n }\n\n /** The whole figure in view: the board's fit, then the 3D camera's own. */\n fit() {\n this.scene?.fit?.('ratio')\n this.scene?.root?.fitCameraToModel?.()\n }\n\n /** The gizmo in a mode on the selected part, as W, E and R do with the canvas focused -- the scene's toggle. */\n gizmo(mode: 'translate' | 'rotate' | 'scale') {\n this.scene?.root?.setGizmoMode?.(mode)\n }\n\n /**\n * Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or\n * one that has outgrown it -- standing up again loses the selection and the camera.\n */\n private standUp() {\n if (!this.stage) return\n const needed = boardFor(this.parts, this.board)\n const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth\n if (this.scene && this.standingFor === this.context) {\n /*\n A part dragged past the board's edge grows the board where it stands. Standing it up again lost the selection,\n the gizmo and the camera after every such drag -- the gizmo seemed not to work at all (user, 2026-09-24).\n */\n if (outgrown) this.grow(needed)\n this.sync()\n return\n }\n this.teardown()\n if (!this.parts.length) return\n this.board = needed\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: needed.width,\n height: needed.height,\n depth: needed.depth,\n threed: true,\n /* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large. */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n ...sceneLook(this.view ?? DEFAULT_VIEW),\n gridStep: niceGridStep(Math.max(needed.width, needed.height)),\n sky: 'color',\n fillStyle: PAPER,\n components: this.components()\n },\n /* Editing mode: the camera view buttons, gizmo modes, help and keys. */\n mode: 1\n }) as SceneHandle\n this.standingFor = this.context\n this.watchScene()\n this.scene.resize?.()\n this.fit()\n this.showSelected()\n } catch (error) {\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n\n private components(): Record<string, unknown>[] {\n const parts = this.parts.map(part => componentOf(part, this.palette, this.board!, this.anchors[ownerOf(part.id)])).filter((c): c is Record<string, unknown> => !!c)\n /* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */\n const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null\n const accent = this.palette['palette.accent'] ?? FAN_COLOUR\n const shown = [...(fan ? [fan] : []), ...this.ghost.map(part => componentOf(part, { ...this.palette, [PREVIEW]: accent, [TARGET_FACE]: accent }, this.board!)), ...(this.highlight ? [componentOf(this.highlight, { ...this.palette, [TARGET_FACE]: accent }, this.board!)] : [])]\n return [...parts, ...shown.filter((c): c is Record<string, unknown> => !!c).map(c => ({ ...c, capturable: false, locked: true }))]\n }\n\n /** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */\n private standing(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /** A larger board under the same scene: its size, and the grid that suits it. Components are re-placed by `sync`. */\n private grow(needed: Board & { depth: number }) {\n const layer = this.scene?.root\n this.board = needed\n if (!layer) return\n this.writing = true\n try {\n layer.set({ width: needed.width, height: needed.height, depth: needed.depth, gridStep: niceGridStep(Math.max(needed.width, needed.height)) })\n } finally {\n this.writing = false\n }\n }\n\n /** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */\n private sync() {\n const layer = this.scene?.root\n if (!layer || !this.board) return\n const wanted = new Map(this.components().map(c => [c.name as string, c]))\n const standing = this.standing()\n this.writing = true\n try {\n for (const [name, component] of standing) {\n if (wanted.has(name)) continue\n layer.removeComponent?.(component)\n component.dispose?.()\n }\n for (const [name, state] of wanted) {\n const found = standing.get(name)\n if (found) {\n const diff = changedKeys(found.state, state)\n if (diff) found.set(diff)\n continue\n }\n const made = Model.compile({ ...state }, layer.app as never) as SceneComponent | null\n if (made) layer.addComponent?.(made)\n }\n } finally {\n this.writing = false\n }\n layer.invalidate?.()\n }\n\n /** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */\n private watchScene() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root?.on || this.viewOnly) return\n root.on('selected', (after: unknown) => {\n const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name\n const id = typeof name === 'string' ? name : ''\n if (ownerOf(id) !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))\n })\n /*\n A change comes up to the board with the component in the last argument's `origin` (the scene appends\n { origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing.\n */\n /*\n A drag is written once, when it ends. The scene's gizmo writes the component on every move -- a size anchored on\n the opposite face, from the scale it captured when the drag began -- and writing each move back through the\n editor (and the evaluation back onto the component) fought it: the scale gizmo did not work (user, 2026-09-24).\n While a pointer is down the changes are only kept; on release the last one is told, and the board is brought to\n the evaluation. A change with no pointer down (the arrow keys) is told after a short pause.\n */\n layer.on?.('change', (...args: unknown[]) => {\n if (this.writing) return\n const origin = (args[args.length - 1] as { origin?: SceneComponent } | undefined)?.origin\n const name = origin?.state?.name\n if (typeof name !== 'string' || !this.parts.some(p => p.id === name)) return\n this.pending.set(name, origin!)\n if (!this.pointerDown) this.commitSoon(80)\n })\n this.stage.addEventListener('pointerdown', this.onPointerDown, true)\n window.addEventListener('pointerup', this.onPointerUp, true)\n window.addEventListener('pointercancel', this.onPointerUp, true)\n }\n\n /** Components the gizmo changed since the last commit, by part name. */\n private pending = new Map<string, SceneComponent>()\n private pointerDown = false\n private commitTimer?: ReturnType<typeof setTimeout>\n private onPointerDown = () => {\n this.pointerDown = true\n }\n private onPointerUp = () => {\n if (!this.pointerDown) return\n this.pointerDown = false\n /* After the scene's own drag end (it settles the depth and rebuilds the geometry on release). */\n this.commitSoon(0)\n }\n private commitSoon(delay: number) {\n clearTimeout(this.commitTimer)\n this.commitTimer = setTimeout(() => this.commit(), delay)\n }\n /** Tell each changed part once, in the inputs' terms; then put the board where the inputs say (a refusal springs back). */\n private commit() {\n /*\n Not while a pointer is down: a pause set by a change just before the press (the arrow keys, the scene settling\n the pick) fired in the middle of the drag and wrote where the part was then -- a drag on a part placed by a rule\n became two steps, the first one a few millimetres in (browser check, 2026-09-25). What is pending waits for the release.\n */\n if (this.pointerDown || !this.board || !this.pending.size) return\n const changed = [...this.pending]\n this.pending.clear()\n for (const [name, component] of changed) {\n const part = this.parts.find(p => p.id === name)\n if (!part) continue\n const result = gizmoResultOf(part, component.state, this.board)\n if (!result.position && !result.rotation && !result.scale) continue\n this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }))\n }\n setTimeout(() => this.sync())\n }\n\n private showSelected() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root) return\n /* A repeat is selected whole: every copy of it (the editor selects the part the author made). */\n const wanted = this.selected ? [...this.standing()].filter(([name]) => ownerOf(name) === this.selected).map(([, component]) => component) : []\n const now = root.selected ?? []\n if (now.length === wanted.length && wanted.every((one, i) => now[i] === one)) return\n root.selected = wanted\n layer.invalidate?.()\n }\n\n /** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */\n private applyView() {\n const layer = this.scene?.root\n if (!layer) return\n layer.set(sceneLook(this.view ?? DEFAULT_VIEW))\n layer.invalidate?.()\n }\n\n private teardown() {\n clearTimeout(this.commitTimer)\n this.pending.clear()\n this.stage?.removeEventListener('pointerdown', this.onPointerDown, true)\n window.removeEventListener('pointerup', this.onPointerUp, true)\n window.removeEventListener('pointercancel', this.onPointerUp, true)\n this.scene?.dispose?.()\n this.scene = undefined\n this.standingFor = undefined\n this.board = undefined\n }\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
  };
@@ -0,0 +1,35 @@
1
+ import type { FigureBodyMove, FigureClipLoss, FigureFaceArea, FigureRowReading } from '@hatiolab/figure-model';
2
+ import type { FigureRenderPart } from './renderer.js';
3
+ export declare const PREVIEW = "__preview";
4
+ export declare const TARGET_FACE = "__target-face";
5
+ export type Say = (key: string, params?: Record<string, string>) => string;
6
+ /** A preview as the editor holds it: bound to the figure, version and command it was worked out for. */
7
+ export interface HeldPreview {
8
+ context: string;
9
+ revision: number;
10
+ /** The command, as text: a preview for another command is not this one's. */
11
+ key: string;
12
+ geometry?: unknown[];
13
+ moves?: FigureBodyMove[];
14
+ /** Bodies it would draw that are not drawn now: a row's copies. */
15
+ added?: string[];
16
+ /** The row it would make, read on the preview's figure. */
17
+ row?: FigureRowReading;
18
+ clipsWouldGo?: FigureClipLoss[];
19
+ face?: FigureFaceArea;
20
+ /** Whether a face was asked for and could not be shown. */
21
+ faceUnknown?: boolean;
22
+ /** Why the command would be refused; nothing else is shown then. */
23
+ refused?: string;
24
+ }
25
+ /** The moving bodies where they would stand, drawn translucent. The ones that stay are not drawn twice. */
26
+ export declare function ghostPartsOf(preview: HeldPreview | undefined): FigureRenderPart[];
27
+ /** The face the part would meet, lit: a thin plate over a flat face, a thin disc over a round one. */
28
+ export declare function faceHighlightOf(face: FigureFaceArea | undefined): FigureRenderPart | null;
29
+ /**
30
+ * What the command would do, in words: the part's own move and turn, what goes with it, the clips it would stop, and
31
+ * why nothing can be shown when the command would be refused.
32
+ */
33
+ export declare function previewWordsOf(preview: HeldPreview | undefined, part: string, say: Say): string[];
34
+ /** A row in words, as figure-model reads it from what is drawn: how many, how far apart, what is left at the ends, what rides it. */
35
+ export declare function rowWordsOf(row: FigureRowReading | undefined, say: Say): string[];
@@ -0,0 +1,95 @@
1
+ /*
2
+ * A command seen before it is done, as the modeller shows it (ADR-0097 §5.5, designer 2026-09-27): the bodies it would
3
+ * move, translucent where they would stand; the face they would meet, lit; and in words, how far and how each turns --
4
+ * the turn as figure-model gives it, one axis and one angle in the figure's frame.
5
+ */
6
+ /* The material tokens of what a command would do: the bodies where they would stand, and the face they would meet. */
7
+ export const PREVIEW = '__preview';
8
+ export const TARGET_FACE = '__target-face';
9
+ /** The moving bodies where they would stand, drawn translucent. The ones that stay are not drawn twice. */
10
+ export function ghostPartsOf(preview) {
11
+ if (!preview?.geometry || !(preview.moves?.length || preview.added?.length))
12
+ return [];
13
+ const moving = new Set([...(preview.moves ?? []).map(m => m.body), ...(preview.added ?? [])]);
14
+ return preview.geometry
15
+ .filter(body => body && typeof body === 'object' && 'shape' in body && 'pose' in body && moving.has(body.id))
16
+ .map(body => ({ ...body, id: `${PREVIEW}:${body.id}`, material: { token: PREVIEW, transparent: true } }));
17
+ }
18
+ /** How thick the lit face is, and how far off the face it floats, so it neither hides nor flickers with it. */
19
+ const THICKNESS = 2;
20
+ const LIFT = 1;
21
+ /** The face the part would meet, lit: a thin plate over a flat face, a thin disc over a round one. */
22
+ export function faceHighlightOf(face) {
23
+ if (!face)
24
+ return null;
25
+ /* Its own x · y · z are the face's two sides and its normal, as the kernel gives them. */
26
+ const r = [0, 1, 2].map(i => [face.u[i], face.normal[i], face.v[i]]);
27
+ const t = [0, 1, 2].map(i => face.centre[i] + face.normal[i] * (LIFT + THICKNESS / 2));
28
+ const shape = face.round
29
+ ? { provider: 'cylinder', dimensions: { radius: face.size.u / 2, length: THICKNESS } }
30
+ : { provider: 'box', dimensions: { width: face.size.u, height: THICKNESS, depth: face.size.v } };
31
+ /* Solid: a translucent plate over a light face was hardly seen (2026-09-27, :3391); it covers the face it names. */
32
+ return { id: TARGET_FACE, shape, pose: { r, t }, material: { token: TARGET_FACE } };
33
+ }
34
+ const mm = (v) => `${v > 0 ? '+' : v < 0 ? '−' : ''}${Math.abs(Math.round(v * 10) / 10).toLocaleString()}`;
35
+ /**
36
+ * What the command would do, in words: the part's own move and turn, what goes with it, the clips it would stop, and
37
+ * why nothing can be shown when the command would be refused.
38
+ */
39
+ export function previewWordsOf(preview, part, say) {
40
+ if (!preview)
41
+ return [];
42
+ if (preview.refused)
43
+ return [say('figure.text.preview-refused', { reason: preview.refused })];
44
+ /* A row: what it would be, read as drawn; its part does not move, it is copied. */
45
+ if (preview.row)
46
+ return [...rowWordsOf(preview.row, say), ...(preview.clipsWouldGo?.length ? [say('figure.text.preview-clips', { clips: preview.clipsWouldGo.map(c => c.clip).join(', ') })] : [])];
47
+ const words = [];
48
+ const own = preview.moves?.find(m => m.body === part);
49
+ if (!own)
50
+ words.push(say('figure.text.preview-stays'));
51
+ else {
52
+ const axes = ['x', 'y', 'z'].map((axis, i) => [axis, own.translate[i]]).filter(([, v]) => Math.abs(v) >= 0.05);
53
+ if (axes.length)
54
+ words.push(say('figure.text.preview-moves', { by: axes.map(([axis, v]) => `${axis.toUpperCase()} ${mm(v)}`).join(' · ') }));
55
+ if (own.turn) {
56
+ /* Signed by the right hand about the axis, as the figure's frame has it. */
57
+ const degrees = mm(own.turn.degrees);
58
+ words.push(typeof own.turn.axis === 'string'
59
+ ? say('figure.text.preview-turns-named', { axis: own.turn.axis.toUpperCase(), degrees })
60
+ : say('figure.text.preview-turns-slanted', { axis: own.turn.axis.map(v => v.toFixed(2)).join(', '), degrees }));
61
+ }
62
+ }
63
+ /* What goes with it: parts fastened to it, each row once, with its count. */
64
+ const others = new Map();
65
+ for (const m of preview.moves ?? [])
66
+ if (m.part !== part)
67
+ others.set(m.part, (others.get(m.part) ?? 0) + 1);
68
+ if (others.size)
69
+ words.push(say('figure.text.preview-carries', { parts: [...others].map(([name, n]) => (n > 1 ? say('figure.text.preview-copies', { part: name, n: String(n) }) : name)).join(', ') }));
70
+ if (preview.clipsWouldGo?.length)
71
+ words.push(say('figure.text.preview-clips', { clips: preview.clipsWouldGo.map(c => c.clip).join(', ') }));
72
+ if (preview.faceUnknown)
73
+ words.push(say('figure.text.preview-face-unknown'));
74
+ return words;
75
+ }
76
+ /** A row in words, as figure-model reads it from what is drawn: how many, how far apart, what is left at the ends, what rides it. */
77
+ export function rowWordsOf(row, say) {
78
+ if (!row)
79
+ return [];
80
+ const words = [
81
+ row.pitch !== undefined
82
+ ? say('figure.text.row-count-pitch', { part: row.part, count: String(row.count), pitch: String(row.pitch), axis: row.axis.toUpperCase() })
83
+ : say('figure.text.row-count-uneven', { part: row.part, count: String(row.count), axis: row.axis.toUpperCase() })
84
+ ];
85
+ if (!row.margins)
86
+ words.push(say('figure.text.row-ends-unknown'));
87
+ else if (row.centred)
88
+ words.push(say('figure.text.row-ends-centred', { mm: String(row.margins.low) }));
89
+ else
90
+ words.push(say('figure.text.row-ends', { low: String(row.margins.low), high: String(row.margins.high) }));
91
+ if (row.companions.length)
92
+ words.push(say('figure.text.row-companions', { parts: row.companions.map(c => say('figure.text.preview-copies', { part: c.part, n: String(c.count) })).join(', ') }));
93
+ return words;
94
+ }
95
+ //# sourceMappingURL=command-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-preview.js","sourceRoot":"","sources":["../../client/modeller/command-preview.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AAEH,sHAAsH;AACtH,MAAM,CAAC,MAAM,OAAO,GAAG,WAAW,CAAA;AAClC,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAA;AAwB1C,2GAA2G;AAC3G,MAAM,UAAU,YAAY,CAAC,OAAgC;IAC3D,IAAI,CAAC,OAAO,EAAE,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,CAAA;IACtF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAC7F,OAAQ,OAAO,CAAC,QAA+B;SAC5C,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;SAC5G,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,IAAI,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAA;AAC7G,CAAC;AAED,+GAA+G;AAC/G,MAAM,SAAS,GAAG,CAAC,CAAA;AACnB,MAAM,IAAI,GAAG,CAAC,CAAA;AAEd,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,IAAgC;IAC9D,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,0FAA0F;IAC1F,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAA;IACvE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,CAAC,IAAI,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAA;IACxF,MAAM,KAAK,GAA8B,IAAI,CAAC,KAAK;QACjD,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;QACtF,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,CAAA;IAClG,oHAAoH;IACpH,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,CAAA;AACrF,CAAC;AAED,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,cAAc,EAAE,EAAE,CAAA;AAElH;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgC,EAAE,IAAY,EAAE,GAAQ;IACrF,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAA;IACvB,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC7F,mFAAmF;IACnF,IAAI,OAAO,CAAC,GAAG;QAAE,OAAO,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACnM,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;IACrD,IAAI,CAAC,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAA;SACjD,CAAC;QACJ,MAAM,IAAI,GAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAE,CAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAA;QACnI,IAAI,IAAI,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,2BAA2B,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5I,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YACb,4EAA4E;YAC5E,MAAM,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACpC,KAAK,CAAC,IAAI,CACR,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ;gBAC/B,CAAC,CAAC,GAAG,CAAC,iCAAiC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC;gBACxF,CAAC,CAAC,GAAG,CAAC,mCAAmC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CACjH,CAAA;QACH,CAAC;IACH,CAAC;IACD,6EAA6E;IAC7E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,KAAK,IAAI,EAAE;QAAE,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC3G,IAAI,MAAM,CAAC,IAAI;QACb,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,4BAA4B,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IACxL,IAAI,OAAO,CAAC,YAAY,EAAE,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3I,IAAI,OAAO,CAAC,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC,CAAA;IAC5E,OAAO,KAAK,CAAA;AACd,CAAC;AAED,qIAAqI;AACrI,MAAM,UAAU,UAAU,CAAC,GAAiC,EAAE,GAAQ;IACpE,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,KAAK,KAAK,SAAS;YACrB,CAAC,CAAC,GAAG,CAAC,6BAA6B,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC1I,CAAC,CAAC,GAAG,CAAC,8BAA8B,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;KACpH,CAAA;IACD,IAAI,CAAC,GAAG,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC,CAAA;SAC5D,IAAI,GAAG,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,8BAA8B,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;;QACjG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,sBAAsB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IAC9G,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,4BAA4B,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,4BAA4B,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IAChM,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import type { FigureBodyMove, FigureClipLoss, FigureFaceArea, FigureRowReading } from '@hatiolab/figure-model'\n\nimport type { FigureRenderPart } from './renderer.js'\n\n/*\n * A command seen before it is done, as the modeller shows it (ADR-0097 §5.5, designer 2026-09-27): the bodies it would\n * move, translucent where they would stand; the face they would meet, lit; and in words, how far and how each turns --\n * the turn as figure-model gives it, one axis and one angle in the figure's frame.\n */\n\n/* The material tokens of what a command would do: the bodies where they would stand, and the face they would meet. */\nexport const PREVIEW = '__preview'\nexport const TARGET_FACE = '__target-face'\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\n/** A preview as the editor holds it: bound to the figure, version and command it was worked out for. */\nexport interface HeldPreview {\n context: string\n revision: number\n /** The command, as text: a preview for another command is not this one's. */\n key: string\n geometry?: unknown[]\n moves?: FigureBodyMove[]\n /** Bodies it would draw that are not drawn now: a row's copies. */\n added?: string[]\n /** The row it would make, read on the preview's figure. */\n row?: FigureRowReading\n clipsWouldGo?: FigureClipLoss[]\n face?: FigureFaceArea\n /** Whether a face was asked for and could not be shown. */\n faceUnknown?: boolean\n /** Why the command would be refused; nothing else is shown then. */\n refused?: string\n}\n\n/** The moving bodies where they would stand, drawn translucent. The ones that stay are not drawn twice. */\nexport function ghostPartsOf(preview: HeldPreview | undefined): FigureRenderPart[] {\n if (!preview?.geometry || !(preview.moves?.length || preview.added?.length)) return []\n const moving = new Set([...(preview.moves ?? []).map(m => m.body), ...(preview.added ?? [])])\n return (preview.geometry as FigureRenderPart[])\n .filter(body => body && typeof body === 'object' && 'shape' in body && 'pose' in body && moving.has(body.id))\n .map(body => ({ ...body, id: `${PREVIEW}:${body.id}`, material: { token: PREVIEW, transparent: true } }))\n}\n\n/** How thick the lit face is, and how far off the face it floats, so it neither hides nor flickers with it. */\nconst THICKNESS = 2\nconst LIFT = 1\n\n/** The face the part would meet, lit: a thin plate over a flat face, a thin disc over a round one. */\nexport function faceHighlightOf(face: FigureFaceArea | undefined): FigureRenderPart | null {\n if (!face) return null\n /* Its own x · y · z are the face's two sides and its normal, as the kernel gives them. */\n const r = [0, 1, 2].map(i => [face.u[i]!, face.normal[i]!, face.v[i]!])\n const t = [0, 1, 2].map(i => face.centre[i]! + face.normal[i]! * (LIFT + THICKNESS / 2))\n const shape: FigureRenderPart['shape'] = face.round\n ? { provider: 'cylinder', dimensions: { radius: face.size.u / 2, length: THICKNESS } }\n : { provider: 'box', dimensions: { width: face.size.u, height: THICKNESS, depth: face.size.v } }\n /* Solid: a translucent plate over a light face was hardly seen (2026-09-27, :3391); it covers the face it names. */\n return { id: TARGET_FACE, shape, pose: { r, t }, material: { token: TARGET_FACE } }\n}\n\nconst mm = (v: number) => `${v > 0 ? '+' : v < 0 ? '−' : ''}${Math.abs(Math.round(v * 10) / 10).toLocaleString()}`\n\n/**\n * What the command would do, in words: the part's own move and turn, what goes with it, the clips it would stop, and\n * why nothing can be shown when the command would be refused.\n */\nexport function previewWordsOf(preview: HeldPreview | undefined, part: string, say: Say): string[] {\n if (!preview) return []\n if (preview.refused) return [say('figure.text.preview-refused', { reason: preview.refused })]\n /* A row: what it would be, read as drawn; its part does not move, it is copied. */\n if (preview.row) return [...rowWordsOf(preview.row, say), ...(preview.clipsWouldGo?.length ? [say('figure.text.preview-clips', { clips: preview.clipsWouldGo.map(c => c.clip).join(', ') })] : [])]\n const words: string[] = []\n const own = preview.moves?.find(m => m.body === part)\n if (!own) words.push(say('figure.text.preview-stays'))\n else {\n const axes = (['x', 'y', 'z'] as const).map((axis, i) => [axis, own.translate[i]!] as const).filter(([, v]) => Math.abs(v) >= 0.05)\n if (axes.length) words.push(say('figure.text.preview-moves', { by: axes.map(([axis, v]) => `${axis.toUpperCase()} ${mm(v)}`).join(' · ') }))\n if (own.turn) {\n /* Signed by the right hand about the axis, as the figure's frame has it. */\n const degrees = mm(own.turn.degrees)\n words.push(\n typeof own.turn.axis === 'string'\n ? say('figure.text.preview-turns-named', { axis: own.turn.axis.toUpperCase(), degrees })\n : say('figure.text.preview-turns-slanted', { axis: own.turn.axis.map(v => v.toFixed(2)).join(', '), degrees })\n )\n }\n }\n /* What goes with it: parts fastened to it, each row once, with its count. */\n const others = new Map<string, number>()\n for (const m of preview.moves ?? []) if (m.part !== part) others.set(m.part, (others.get(m.part) ?? 0) + 1)\n if (others.size)\n words.push(say('figure.text.preview-carries', { parts: [...others].map(([name, n]) => (n > 1 ? say('figure.text.preview-copies', { part: name, n: String(n) }) : name)).join(', ') }))\n if (preview.clipsWouldGo?.length) words.push(say('figure.text.preview-clips', { clips: preview.clipsWouldGo.map(c => c.clip).join(', ') }))\n if (preview.faceUnknown) words.push(say('figure.text.preview-face-unknown'))\n return words\n}\n\n/** A row in words, as figure-model reads it from what is drawn: how many, how far apart, what is left at the ends, what rides it. */\nexport function rowWordsOf(row: FigureRowReading | undefined, say: Say): string[] {\n if (!row) return []\n const words = [\n row.pitch !== undefined\n ? say('figure.text.row-count-pitch', { part: row.part, count: String(row.count), pitch: String(row.pitch), axis: row.axis.toUpperCase() })\n : say('figure.text.row-count-uneven', { part: row.part, count: String(row.count), axis: row.axis.toUpperCase() })\n ]\n if (!row.margins) words.push(say('figure.text.row-ends-unknown'))\n else if (row.centred) words.push(say('figure.text.row-ends-centred', { mm: String(row.margins.low) }))\n else words.push(say('figure.text.row-ends', { low: String(row.margins.low), high: String(row.margins.high) }))\n if (row.companions.length) words.push(say('figure.text.row-companions', { parts: row.companions.map(c => say('figure.text.preview-copies', { part: c.part, n: String(c.count) })).join(', ') }))\n return words\n}\n"]}
@@ -18,6 +18,11 @@ import type { FirstSave } from './first-save.js';
18
18
  export declare class FigureEditor extends LitElement {
19
19
  static styles: import("lit").CSSResult[];
20
20
  session?: FigureProductSession;
21
+ /** The saved figure's state and version, as the server gave them (the page reads the row). */
22
+ releaseState?: {
23
+ state?: 'draft' | 'released';
24
+ version: number;
25
+ };
21
26
  /** The recipe templates (ADR-0095), read by the page from the server. */
22
27
  templates: RecipeTemplate[];
23
28
  /** Reads a template's guide; given by the page. */
@@ -62,9 +67,17 @@ export declare class FigureEditor extends LitElement {
62
67
  * Parts hidden on the workbench — the author's view while working, not the asset's visibility. Not saved: a part
63
68
  * hidden here is still in the figure, still checked, still drawn on a board.
64
69
  */
70
+ /** The page offers an AI draft for a new figure (the modeller page, through the app's assistant dock). */
71
+ aiDraft: boolean;
65
72
  private hiddenParts;
66
73
  /** A value in hand -- typed, held on a spinner, scrubbed, dragged -- drawn at once and recorded on release. */
67
74
  private live;
75
+ /**
76
+ * A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it
77
+ * names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone.
78
+ */
79
+ private preview?;
80
+ private previewDrawn?;
68
81
  private lastContext;
69
82
  /**
70
83
  * Move, turn and size on the canvas, in the bar's tools: the gizmo the W · E · R keys give, found without knowing the
@@ -76,6 +89,11 @@ export declare class FigureEditor extends LitElement {
76
89
  private onKey;
77
90
  connectedCallback(): void;
78
91
  disconnectedCallback(): void;
92
+ private static readonly FOLD_START_UNDER;
93
+ private foldedForWidth;
94
+ private keptOpenByAuthor;
95
+ private widthWatch;
96
+ private fitPanesTo;
79
97
  /**
80
98
  * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
81
99
  * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
@@ -84,7 +102,13 @@ export declare class FigureEditor extends LitElement {
84
102
  */
85
103
  private toldViewError;
86
104
  private tell;
105
+ private askClipLoss;
106
+ private confirmClipLoss;
87
107
  private ask;
108
+ private panelMemory;
109
+ private restorePanelFor?;
110
+ private propertiesPanel;
111
+ private foldsOf;
88
112
  protected willUpdate(_changed: PropertyValues): void;
89
113
  /** The scene toggles: the mode already shown takes the gizmo away. */
90
114
  private useGizmo;
@@ -131,12 +155,25 @@ export declare class FigureEditor extends LitElement {
131
155
  private designValues;
132
156
  /** What the gizmo may write for the selected part — read off the asset being edited. */
133
157
  private reachOf;
158
+ /** Whether a size drag on this part could not show what is written: figure-model gives no face it keeps. */
159
+ private sizeDragUnsure;
134
160
  /** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
135
161
  private transformed;
136
162
  /** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
137
163
  private align;
138
164
  /** A mirrored copy: duplicate, then place the copy on the other side — two steps. */
139
165
  private mirror;
166
+ /**
167
+ * The one-off "put it on the plane", by the figure's placement (designer 2026-09-27): figure-model's rule
168
+ * (`figurePlaneAlignChanges`) -- a floor figure's part to the floor by its bottom, a ceiling one's to the ceiling by
169
+ * its top, a wall one's to the wall by its back, a centred one's centre to the origin's height. Once: nothing is
170
+ * fastened. Refused, it says why.
171
+ */
172
+ private keptExtremeWord;
173
+ private planeContact?;
174
+ private planeContactOf;
175
+ private renderPlaneAlign;
176
+ private alignToPlane;
140
177
  private renderTools;
141
178
  /**
142
179
  * A part's name. It renames every value the part owns, and what refers to them follows
@@ -150,8 +187,21 @@ export declare class FigureEditor extends LitElement {
150
187
  private renamePart;
151
188
  /** Size response, fastening and motion for the selected part, as authoring commands. */
152
189
  private renderLinks;
190
+ /**
191
+ * The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
192
+ * while dragging and the commit keeps it, so nothing jumps on release. A part whose fastening is turned has none the
193
+ * rule can say, and the gizmo keeps its own (the face across from the handle).
194
+ */
195
+ private anchorsOf;
196
+ private lastAnchors;
153
197
  /** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
154
198
  private renderControls;
199
+ /**
200
+ * "Follow the size and keep where the figure is placed" (ADR-0097): figure-model plans it from the figure as it is now,
201
+ * and the whole plan is one step of the history. A refusal with a way on -- move the part onto the plane, replace how
202
+ * its position is worked out -- asks in the app's prompt and plans again with that choice; any other says why.
203
+ */
204
+ private resizeWithPlacement;
155
205
  /** One command, one step of the history. A refusal a person can meet here is said in their words. */
156
206
  private linkCommand;
157
207
  private changeField;
@@ -184,6 +234,8 @@ export declare class FigureEditor extends LitElement {
184
234
  * time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
185
235
  */
186
236
  private renderRepeat;
237
+ /** The selected row as it is drawn now, read by figure-model: nothing when it cannot be read. */
238
+ private rowNow;
187
239
  /** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
188
240
  private pitchInputOf;
189
241
  /**
@@ -198,15 +250,75 @@ export declare class FigureEditor extends LitElement {
198
250
  */
199
251
  private setPitch;
200
252
  /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
253
+ private releaseWords;
254
+ /** To the release that exists: the history tab's release section, not a second one here. */
255
+ private goRelease;
256
+ /** The fastening form's command, seen before it is done; null clears what was shown. */
257
+ private previewCommand;
258
+ /** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
259
+ private currentPreview;
260
+ private detachSaid?;
261
+ private detachWords;
262
+ private overhangSaid?;
263
+ /** Wider than its face: a fact of the relation, said in its details, not as a warning (designer 2026-09-27). */
264
+ private overhangNote;
265
+ private overhangWords;
266
+ /** The selected part's motion at both ends of its range, made once per figure, version and part. */
267
+ private motionEndsSaid?;
268
+ private motionEnds;
269
+ /** "Fasten it first", from the motion tab: to the properties tab, the fastening form open on this part. */
270
+ private goFasten;
271
+ /** What the canvas draws of it, made once per preview: a new list every render would stand the board up each time. */
272
+ private previewShown;
273
+ /**
274
+ * What the author has in view, for the AI dock to hear with a request (ADR-0097 §5.6 item 5): the part selected and
275
+ * what the preview is trying. Read at the moment of sending, never cached, and never a reason to start a new
276
+ * conversation -- selecting is not a new subject.
277
+ */
278
+ assistantFocus(): {
279
+ part?: string;
280
+ trying?: {
281
+ clips?: Record<string, number>;
282
+ controls?: Record<string, number>;
283
+ };
284
+ };
201
285
  private choose;
202
286
  private asset;
287
+ /** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
203
288
  private renderTab;
204
289
  updated(changed: PropertyValues<this>): void;
205
- /** Which part the panel speaks of, with the way back to the whole figure. */
290
+ /**
291
+ * A new figure's first question, on the canvas where the work will be (designer 2026-09-27, U04): a basic shape, a
292
+ * recipe, or an AI draft when the page offers one. Nothing is added until one is chosen; the type, the space, the
293
+ * mounting face and the tags are not asked for first.
294
+ */
295
+ private renderStart;
296
+ /**
297
+ * Where the panel is, held at its top: the model by its name -- the way back to its settings -- and the part chosen,
298
+ * with the part's three lines: its kind, how its size answers, what holds it (designer 2026-09-27, B and B.1). The
299
+ * model name is the same move as the parts list's root; there is no separate "whole figure" link.
300
+ */
206
301
  private renderSubject;
302
+ /**
303
+ * The model's settings: the same move from the parts list's root and from the panel's path (B.1). It shows them on
304
+ * the properties tab whatever tab was open; it changes no data, no camera, no history and no AI conversation.
305
+ */
306
+ private showModelSettings;
207
307
  render(): import("lit-html").TemplateResult<1>;
208
308
  /** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
209
309
  private renderGroups;
310
+ /**
311
+ * A box-like part's size row stands when a rule sets every side -- each following the figure's size -- with the
312
+ * numbers the rule gives, as its position and turn rows do (designer 2026-09-27: not a blank, not gone).
313
+ */
314
+ private withSizeRow;
210
315
  private renderVector;
316
+ /**
317
+ * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
318
+ * typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.
319
+ */
320
+ private renderRoomOnly;
321
+ /** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
322
+ private askRoomRefit;
211
323
  private renderSingle;
212
324
  }