@things-factory/figure-ui 10.1.91 → 10.1.93

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 (56) hide show
  1. package/client/modeller/editor.ts +61 -90
  2. package/client/modeller/fasten-geometry.ts +152 -0
  3. package/client/modeller/fields.ts +8 -1
  4. package/client/modeller/figure-cavity-drawing.ts +259 -0
  5. package/client/modeller/figure-fasten-drawing.ts +406 -0
  6. package/client/modeller/figure-links.ts +71 -35
  7. package/client/modeller/figure-section-close-choice.ts +52 -0
  8. package/client/modeller/figure-section-editor.ts +58 -3
  9. package/client/modeller/figure-section-panel.ts +5 -0
  10. package/client/modeller/section-controls.ts +19 -0
  11. package/client/modeller/side-panel-styles.ts +4 -11
  12. package/dist-client/modeller/editor.d.ts +10 -4
  13. package/dist-client/modeller/editor.js +59 -88
  14. package/dist-client/modeller/editor.js.map +1 -1
  15. package/dist-client/modeller/fasten-geometry.d.ts +60 -0
  16. package/dist-client/modeller/fasten-geometry.js +138 -0
  17. package/dist-client/modeller/fasten-geometry.js.map +1 -0
  18. package/dist-client/modeller/fields.js +8 -1
  19. package/dist-client/modeller/fields.js.map +1 -1
  20. package/dist-client/modeller/figure-cavity-drawing.d.ts +48 -0
  21. package/dist-client/modeller/figure-cavity-drawing.js +253 -0
  22. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -0
  23. package/dist-client/modeller/figure-fasten-drawing.d.ts +74 -0
  24. package/dist-client/modeller/figure-fasten-drawing.js +452 -0
  25. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -0
  26. package/dist-client/modeller/figure-links.d.ts +7 -0
  27. package/dist-client/modeller/figure-links.js +70 -33
  28. package/dist-client/modeller/figure-links.js.map +1 -1
  29. package/dist-client/modeller/figure-section-close-choice.d.ts +17 -0
  30. package/dist-client/modeller/figure-section-close-choice.js +58 -0
  31. package/dist-client/modeller/figure-section-close-choice.js.map +1 -0
  32. package/dist-client/modeller/figure-section-editor.d.ts +11 -1
  33. package/dist-client/modeller/figure-section-editor.js +57 -3
  34. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  35. package/dist-client/modeller/figure-section-panel.d.ts +1 -0
  36. package/dist-client/modeller/figure-section-panel.js +5 -0
  37. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  38. package/dist-client/modeller/section-controls.d.ts +10 -0
  39. package/dist-client/modeller/section-controls.js +17 -0
  40. package/dist-client/modeller/section-controls.js.map +1 -1
  41. package/dist-client/modeller/side-panel-styles.js +4 -11
  42. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  43. package/dist-client/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -2
  45. package/test/browser/acceptance-page.js +10 -0
  46. package/test/cavity-drawing.test.ts +69 -0
  47. package/test/fasten-drawing.test.ts +45 -0
  48. package/test/fasten-geometry.test.ts +79 -0
  49. package/test/layout.test.ts +19 -4
  50. package/test/links.test.ts +3 -1
  51. package/test/section-editor.test.ts +31 -8
  52. package/translations/en.json +29 -15
  53. package/translations/ja.json +25 -11
  54. package/translations/ko.json +29 -15
  55. package/translations/ms.json +25 -11
  56. package/translations/zh.json +25 -11
@@ -8,6 +8,9 @@ import type { FigureAuthoringAction, FigureEvaluatedShape, FigureSection, Figure
8
8
  import { SidePanelStyles } from './side-panel-styles.js'
9
9
  import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, type SectionWorking } from './section-drawing.js'
10
10
  import { END_FACE } from './section-controls.js'
11
+ import { openPopup } from '@operato/layout'
12
+ import './figure-section-close-choice.js'
13
+ import type { SectionCloseChoice } from './figure-section-close-choice.js'
11
14
 
12
15
  type Axis = 'x' | 'y' | 'z'
13
16
 
@@ -34,7 +37,8 @@ export const NARROW_BELOW = 900
34
37
  *
35
38
  * What the kernel would refuse is marked on the outline and said in the list; apply is held back, and the solid shows
36
39
  * the last valid state, which the caption names. Apply sends one `set-section` command (one step of the history); cancel
37
- * or Esc sends nothing and lets the preview go. The undo here is the editor's own. If the figure changes while the editor
40
+ * or Esc sends nothing and lets the preview go -- after asking, when something was changed: apply it, let it go, or keep
41
+ * editing (`requestClose`). The undo here is the editor's own. If the figure changes while the editor
38
42
  * is open, apply is refused until it is opened again.
39
43
  */
40
44
  @customElement('figure-section-editor')
@@ -306,10 +310,12 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
306
310
  */
307
311
  private keys = (e: KeyboardEvent) => {
308
312
  const inField = (e.composedPath()[0] as HTMLElement)?.tagName === 'INPUT'
313
+ /* While the close is being asked, the question has the keys: Esc there is 계속 편집. */
314
+ if (this.asking) return
309
315
  const take = () => (e.preventDefault(), e.stopImmediatePropagation())
310
316
  if (e.key === 'Escape') {
311
317
  take()
312
- this.cancel()
318
+ this.requestClose()
313
319
  } else if ((e.key === 'Delete' || e.key === 'Backspace') && !inField && this.selected) {
314
320
  /* In a field the key deletes a character; outside, the chosen point. */
315
321
  take()
@@ -390,6 +396,55 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
390
396
  this.close()
391
397
  }
392
398
 
399
+ /** Whether anything was changed since the editor opened. */
400
+ get dirty(): boolean {
401
+ const key = (w: SectionWorking) =>
402
+ JSON.stringify([
403
+ w.points.map(p => [p.id, p.x ?? 0, p.y ?? 0, p.z ?? 0]),
404
+ Object.entries(w.across).sort(),
405
+ w.length,
406
+ w.apex ? Object.entries(w.apex).sort() : null
407
+ ])
408
+ return !!this.opened && key(this.working) !== key(this.opened)
409
+ }
410
+
411
+ private asking = false
412
+ /**
413
+ * Close, asking first when something was changed (user 2026-10-01): apply it, let it go, or keep editing. True when the
414
+ * editor closed; false when the person keeps editing.
415
+ */
416
+ async requestClose(): Promise<boolean> {
417
+ if (!this.dirty) {
418
+ this.cancel()
419
+ return true
420
+ }
421
+ if (this.asking) return false
422
+ const held = this.stale
423
+ ? i18next.t('figure.text.section-editor-stale')
424
+ : this.valid(this.working)
425
+ ? ''
426
+ : i18next.t('figure.text.section-editor-close-invalid')
427
+ this.asking = true
428
+ try {
429
+ const choice = await openPopup<SectionCloseChoice>(html`<figure-section-close-choice .held=${held}></figure-section-close-choice>`, {
430
+ backdrop: true,
431
+ size: 'small',
432
+ title: i18next.t('figure.label.section-editor')
433
+ }).result
434
+ if (choice === 'apply' && !held) {
435
+ this.apply()
436
+ return true
437
+ }
438
+ if (choice === 'discard') {
439
+ this.cancel()
440
+ return true
441
+ }
442
+ return false
443
+ } finally {
444
+ this.asking = false
445
+ }
446
+ }
447
+
393
448
  private cancel() {
394
449
  this.dispatchEvent(new CustomEvent('command-live', { detail: { action: null }, bubbles: true, composed: true }))
395
450
  this.close()
@@ -441,7 +496,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
441
496
  ? i18next.t('figure.text.section-editor-stale')
442
497
  : i18next.t(this.kind === 'polygon' ? 'figure.text.section-editor-hint' : 'figure.text.section-editor-hint-sized')}
443
498
  </span>
444
- <button @click=${() => this.cancel()}>${i18next.t('figure.button.section-editor-cancel')}</button>
499
+ <button @click=${() => this.requestClose()}>${i18next.t('figure.button.section-editor-cancel')}</button>
445
500
  <button primary ?disabled=${this.stale || words.length > 0} @click=${() => this.apply()}>${i18next.t('figure.button.section-editor-apply')}</button>
446
501
  </footer>
447
502
  `
@@ -6,6 +6,7 @@ import { i18next, localize } from '@operato/i18n'
6
6
  import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'
7
7
  import { SidePanelStyles } from './side-panel-styles.js'
8
8
  import { drawnOf } from './section-drawing.js'
9
+ import './figure-cavity-drawing.js'
9
10
  import {
10
11
  END_FACE,
11
12
  SIDE_FACE,
@@ -106,6 +107,9 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
106
107
  <button act ?disabled=${this.locked} @click=${() => this.dispatchEvent(new CustomEvent('section-editor-open', { bubbles: true, composed: true }))}>
107
108
  ${i18next.t('figure.button.section-editor-open')}
108
109
  </button>
110
+ ${e.section.kind === 'polygon'
111
+ ? html`<small>${i18next.t(e.apex ? 'figure.text.section-points-apex' : 'figure.text.section-points', { points: String(e.section.points.length) })}</small>`
112
+ : nothing}
109
113
  </div>`
110
114
  : nothing}
111
115
  ${kind === 'rectangle' ? this.renderCorner(body) : nothing}
@@ -185,6 +189,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
185
189
  </small>`
186
190
  : nothing}
187
191
  </div>
192
+ ${cavity ? html`<figure-cavity-drawing .body=${body} .perSide=${this.perSide} ?locked=${this.locked}></figure-cavity-drawing>` : nothing}
188
193
  ${cavity
189
194
  ? html`<div field>
190
195
  <label>${i18next.t('figure.label.section-wall')}</label>
@@ -113,3 +113,22 @@ export function cavityDepthOf(shape: FigureEvaluatedShape): number {
113
113
  const closed = (x: typeof c.start) => (x === 'open' ? 0 : x.closed)
114
114
  return mm(e.end - e.start - closed(c.start) - closed(c.end))
115
115
  }
116
+
117
+ /** The inside as it stands, every side's wall written out. */
118
+ export function cavityWorkingOf(shape: FigureEvaluatedShape): { wall: Record<string, number>; start: FigureCavityRequest['start']; end: FigureCavityRequest['end'] } | undefined {
119
+ const c = shape.cavity
120
+ const e = shape.extrusion
121
+ if (!c || !e) return undefined
122
+ return { wall: Object.fromEntries(sidesOf(e.axis).map(s => [s, c.wall[s]!])), start: c.start, end: c.end }
123
+ }
124
+
125
+ /** A wall dragged to `value`: that side alone, or every side when the walls are one. */
126
+ export function wallDragged(wall: Record<string, number>, side: string, value: number, perSide: boolean): Record<string, number> {
127
+ const v = Math.max(1, Math.round(value))
128
+ return perSide ? { ...wall, [side]: v } : Object.fromEntries(Object.keys(wall).map(s => [s, v]))
129
+ }
130
+
131
+ /** An end pressed: open becomes closed by the default wall, closed becomes open. */
132
+ export function endToggled(shape: FigureEvaluatedShape, now: FigureCavityRequest['start']): FigureCavityRequest['start'] {
133
+ return now === 'open' ? { closed: defaultWallOf(shape) } : 'open'
134
+ }
@@ -101,19 +101,17 @@ export const SidePanelStyles = css`
101
101
  div[vector-row] + div[inline-field] {
102
102
  margin-top: 5px;
103
103
  }
104
- /* A row of icon tools (align, mirror, snap). */
104
+ /* A row of worded tools (align, snap; mirror copies). */
105
105
  div[tools] {
106
106
  display: flex;
107
107
  flex-wrap: wrap;
108
108
  gap: 4px;
109
109
  }
110
110
  div[tools] button {
111
- display: inline-flex;
112
- align-items: center;
113
- justify-content: center;
114
- width: 30px;
115
111
  height: 28px;
116
- padding: 0;
112
+ padding: 0 10px;
113
+ font: inherit;
114
+ font-size: 0.78rem;
117
115
  color: var(--md-sys-color-on-surface);
118
116
  background: var(--md-sys-color-surface-container-lowest);
119
117
  border: 1px solid var(--md-sys-color-outline-variant);
@@ -127,11 +125,6 @@ export const SidePanelStyles = css`
127
125
  opacity: 0.45;
128
126
  cursor: default;
129
127
  }
130
- div[tools] md-icon {
131
- font-size: 17px;
132
- width: 17px;
133
- height: 17px;
134
- }
135
128
  div[pair] {
136
129
  display: flex;
137
130
  gap: 4px;
@@ -114,6 +114,12 @@ export declare class FigureEditor extends LitElement {
114
114
  private propertiesPanel;
115
115
  private foldsOf;
116
116
  protected willUpdate(_changed: PropertyValues): void;
117
+ /**
118
+ * The selection moved off the part being edited. With nothing changed there the editor closes; with changes, the part
119
+ * stays selected while the editor asks (apply, let go, keep editing), and the move goes on only once it closed. A part
120
+ * gone or another figure opened has nothing to go back to: the editor closes.
121
+ */
122
+ private leaveSectionEditor;
117
123
  /** The scene toggles: the mode already shown takes the gizmo away. */
118
124
  private useGizmo;
119
125
  /**
@@ -303,13 +309,13 @@ export declare class FigureEditor extends LitElement {
303
309
  private pointBlockedOf;
304
310
  private renderStart;
305
311
  /**
306
- * Where the panel is, held at its top: the model by its name -- the way back to its settings -- and the part chosen,
307
- * with the part's three lines: its kind, how its size answers, what holds it (designer 2026-09-27, B and B.1). The
308
- * model name is the same move as the parts list's root; there is no separate "whole figure" link.
312
+ * The part the panel speaks of, held at its top: its kind, how its size answers, what holds it (designer 2026-09-27,
313
+ * B). No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
314
+ * list's root is the way back to the model's settings.
309
315
  */
310
316
  private renderSubject;
311
317
  /**
312
- * 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
318
+ * The model's settings, from the parts list's root (B.1). It shows them on
313
319
  * the properties tab whatever tab was open; it changes no data, no camera, no history and no AI conversation.
314
320
  */
315
321
  private showModelSettings;
@@ -379,7 +379,7 @@ let FigureEditor = class FigureEditor extends LitElement {
379
379
  [off] {
380
380
  display: none !important;
381
381
  }
382
- /* Which part the panel speaks of, and the way back to the whole figure. */
382
+ /* Which part the panel speaks of. */
383
383
  div[subject] {
384
384
  position: sticky;
385
385
  top: 0;
@@ -390,41 +390,6 @@ let FigureEditor = class FigureEditor extends LitElement {
390
390
  background: var(--md-sys-color-surface-container-lowest);
391
391
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
392
392
  }
393
- nav[path] {
394
- display: flex;
395
- align-items: center;
396
- gap: 6px;
397
- min-width: 0;
398
- }
399
- nav[path] button[model-crumb] {
400
- flex: none;
401
- max-width: 55%;
402
- height: 28px;
403
- padding: 0 8px;
404
- overflow: hidden;
405
- white-space: nowrap;
406
- text-overflow: ellipsis;
407
- border: 1px solid var(--md-sys-color-outline-variant);
408
- border-radius: 999em;
409
- background: none;
410
- color: var(--md-sys-color-primary);
411
- cursor: pointer;
412
- }
413
- nav[path] button[model-crumb][on] {
414
- border-color: var(--md-sys-color-primary);
415
- background: var(--md-sys-color-primary-container);
416
- }
417
- nav[path] button[model-crumb]:focus-visible {
418
- outline: 2px solid var(--md-sys-color-primary);
419
- outline-offset: 1px;
420
- }
421
- nav[path] span[current] {
422
- min-width: 0;
423
- overflow: hidden;
424
- white-space: nowrap;
425
- text-overflow: ellipsis;
426
- font-weight: 600;
427
- }
428
393
  div[subject-summary] {
429
394
  display: grid;
430
395
  gap: 1px;
@@ -638,7 +603,27 @@ let FigureEditor = class FigureEditor extends LitElement {
638
603
  * own Esc, the part removed, another figure opened -- it closes and lets go of the preview it held.
639
604
  */
640
605
  if (this.sectionEditing && this.sectionEditing !== this.selected)
606
+ this.leaveSectionEditor(this.selected);
607
+ }
608
+ /**
609
+ * The selection moved off the part being edited. With nothing changed there the editor closes; with changes, the part
610
+ * stays selected while the editor asks (apply, let go, keep editing), and the move goes on only once it closed. A part
611
+ * gone or another figure opened has nothing to go back to: the editor closes.
612
+ */
613
+ leaveSectionEditor(next) {
614
+ const editing = this.sectionEditing;
615
+ const editor = this.renderRoot.querySelector('figure-section-editor');
616
+ const view = this.session?.read();
617
+ const stillThere = !!view?.geometry.some(g => ownerOf(g.id) === editing);
618
+ if (!editor?.dirty || !stillThere) {
641
619
  this.closeSectionEditor();
620
+ return;
621
+ }
622
+ this.selected = editing;
623
+ editor.requestClose().then(closed => {
624
+ if (closed && this.selected === editing)
625
+ this.selected = next;
626
+ });
642
627
  }
643
628
  /** The scene toggles: the mode already shown takes the gizmo away. */
644
629
  useGizmo(mode) {
@@ -1019,14 +1004,11 @@ let FigureEditor = class FigureEditor extends LitElement {
1019
1004
  }
1020
1005
  renderPlaneAlign(asset, unavailable) {
1021
1006
  const placement = asset.placement ?? 'floor';
1022
- const icon = { floor: 'vertical_align_bottom', ceiling: 'vertical_align_top', wall: 'flip_to_back', center: 'vertical_align_center' }[placement];
1023
1007
  /* A tilted part touches the plane at a point or an edge, a ball at its lowest; the tooltip says so rather than promise a face (designer 2026-09-27). */
1024
1008
  const contact = this.planeContactOf(this.session.source(), this.selected);
1025
1009
  const told = contact === 'tilted' || contact === 'extreme' ? `-${contact}` : '';
1026
- return html `<button plane-align=${placement} ?disabled=${unavailable} aria-label=${i18next.t(`figure.button.align-to-${placement}`)}
1027
- title=${i18next.t(`figure.text.align-to-${placement}${told}`)} @click=${() => this.alignToPlane()}>
1028
- <md-icon>${icon}</md-icon>
1029
- </button>`;
1010
+ return html `<button plane-align=${placement} ?disabled=${unavailable} title=${i18next.t(`figure.text.align-to-${placement}${told}`)}
1011
+ @click=${() => this.alignToPlane()}>${i18next.t(`figure.button.align-to-${placement}`)}</button>`;
1030
1012
  }
1031
1013
  alignToPlane() {
1032
1014
  const asset = parseFigureAsset(this.session.source());
@@ -1051,26 +1033,30 @@ let FigureEditor = class FigureEditor extends LitElement {
1051
1033
  catch {
1052
1034
  return null;
1053
1035
  }
1054
- const tool = (name, icon, label, title) => html `<button ?disabled=${unavailable} aria-label=${i18next.t(label)} title=${i18next.t(title)} @click=${() => this.align(name)}>
1055
- <md-icon>${icon}</md-icon>
1056
- </button>`;
1057
- return html `<div tools title=${i18next.t('figure.text.tools-explained')}>
1058
- ${tool('centre-x', 'horizontal_distribute', 'figure.button.center-x', 'figure.text.center-on-x')}
1059
- ${tool('centre-z', 'vertical_align_center', 'figure.button.center-z', 'figure.text.center-on-z')}
1060
- ${this.renderPlaneAlign(asset, unavailable)}
1061
- ${['x', 'z'].map(axis => {
1062
- const able = canMirror(asset, this.selected, axis, reach);
1063
- return html `<button
1064
- ?disabled=${unavailable || !able}
1065
- aria-label=${i18next.t('figure.button.mirror-' + axis)}
1066
- title=${i18next.t(able ? 'figure.text.mirror-explained' : 'figure.text.mirror-would-overlap')}
1067
- @click=${() => this.mirror(axis)}
1068
- >
1069
- <md-icon>${axis === 'x' ? 'flip' : 'flip_camera_android'}</md-icon>
1070
- </button>`;
1071
- })}
1072
- ${tool('snap', 'grid_on', 'figure.button.snap', 'figure.text.snap-explained')}
1073
- </div>`;
1036
+ /*
1037
+ * In words, two rows by what they do (user 2026-10-01: 「무엇을 하는 것인지 모르겠다」): moving the part to the figure's
1038
+ * centre lines, onto its plane or onto the grid; and making a mirrored copy, which makes a part and moves nothing.
1039
+ */
1040
+ const tool = (name, label, title) => html `<button ?disabled=${unavailable} title=${i18next.t(title)} @click=${() => this.align(name)}>${i18next.t(label)}</button>`;
1041
+ const mirrors = ['x', 'z'].map(axis => ({ axis, able: canMirror(asset, this.selected, axis, reach) }));
1042
+ return html `<div vector-row>
1043
+ <label>${i18next.t('figure.label.tools')}</label>
1044
+ <div tools>
1045
+ ${tool('centre-x', 'figure.button.center-x', 'figure.text.center-on-x')}
1046
+ ${tool('centre-z', 'figure.button.center-z', 'figure.text.center-on-z')}
1047
+ ${this.renderPlaneAlign(asset, unavailable)}
1048
+ ${tool('snap', 'figure.button.snap', 'figure.text.snap-explained')}
1049
+ </div>
1050
+ </div>
1051
+ <div vector-row>
1052
+ <label>${i18next.t('figure.label.mirror-copy')}</label>
1053
+ <div tools>
1054
+ ${mirrors.map(({ axis, able }) => html `<button ?disabled=${unavailable || !able} title=${i18next.t('figure.text.mirror-explained')} @click=${() => this.mirror(axis)}>
1055
+ ${i18next.t('figure.button.mirror-' + axis)}
1056
+ </button>`)}
1057
+ </div>
1058
+ </div>
1059
+ ${mirrors.every(m => !m.able) ? html `<p hint>${i18next.t('figure.text.mirror-would-overlap')}</p>` : nothing}`;
1074
1060
  }
1075
1061
  /**
1076
1062
  * A part's name. It renames every value the part owns, and what refers to them follows
@@ -1906,12 +1892,11 @@ let FigureEditor = class FigureEditor extends LitElement {
1906
1892
  </div>`;
1907
1893
  }
1908
1894
  /**
1909
- * Where the panel is, held at its top: the model by its name -- the way back to its settings -- and the part chosen,
1910
- * with the part's three lines: its kind, how its size answers, what holds it (designer 2026-09-27, B and B.1). The
1911
- * model name is the same move as the parts list's root; there is no separate "whole figure" link.
1895
+ * The part the panel speaks of, held at its top: its kind, how its size answers, what holds it (designer 2026-09-27,
1896
+ * B). No path above it (user 2026-10-01): the parts list and the canvas already show which part is chosen, and the
1897
+ * list's root is the way back to the model's settings.
1912
1898
  */
1913
1899
  renderSubject() {
1914
- const name = this.session?.read()?.name || i18next.t('figure.text.new-figure-name');
1915
1900
  let summary;
1916
1901
  try {
1917
1902
  summary = this.selected ? partSummaryOf(parseFigureAsset(this.session.source()), ownerOf(this.selected), (key, params) => i18next.t(key, params)) : undefined;
@@ -1919,24 +1904,17 @@ let FigureEditor = class FigureEditor extends LitElement {
1919
1904
  catch {
1920
1905
  summary = undefined;
1921
1906
  }
1907
+ if (!summary)
1908
+ return nothing;
1922
1909
  return html `<div subject>
1923
- <nav path aria-label=${i18next.t('figure.label.panel-path')}>
1924
- <button model-crumb ?on=${!this.selected} title=${`${i18next.t('figure.button.show-model-settings')} · ${name}`}
1925
- aria-label=${`${i18next.t('figure.button.show-model-settings')}: ${name}`} @click=${() => this.showModelSettings()}>${name}</button>
1926
- ${this.selected
1927
- ? html `<span aria-hidden="true">›</span><span current aria-current="location" title=${partLabelOf(this.selected)}>${partLabelOf(this.selected)}</span>`
1928
- : html `<span aria-hidden="true">·</span><span current aria-current="location">${i18next.t('figure.label.model-settings')}</span>`}
1929
- </nav>
1930
- ${summary
1931
- ? html `<div subject-summary>
1932
- ${summary.kind ? html `<b>${summary.kind}</b>` : nothing}
1933
- <small>${summary.size}</small><small>${summary.relation}</small>
1934
- </div>`
1935
- : nothing}
1910
+ <div subject-summary>
1911
+ ${summary.kind ? html `<b>${summary.kind}</b>` : nothing}
1912
+ <small>${summary.size}</small><small>${summary.relation}</small>
1913
+ </div>
1936
1914
  </div>`;
1937
1915
  }
1938
1916
  /**
1939
- * 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
1917
+ * The model's settings, from the parts list's root (B.1). It shows them on
1940
1918
  * the properties tab whatever tab was open; it changes no data, no camera, no history and no AI conversation.
1941
1919
  */
1942
1920
  showModelSettings() {
@@ -2172,7 +2150,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2172
2150
  ? html `<section>
2173
2151
  <h3>${i18next.t(vectorTitle)}</h3>
2174
2152
  ${groups.vectors.map(g => this.renderVector(g, unavailable, held))}
2175
- ${tools ? html `<div vector-row><label>${i18next.t('figure.label.tools')}</label>${tools}</div>` : nothing}
2153
+ ${tools ?? nothing}
2176
2154
  </section>`
2177
2155
  : nothing}
2178
2156
  ${groups.singles.length
@@ -2282,14 +2260,7 @@ let FigureEditor = class FigureEditor extends LitElement {
2282
2260
  this.linkCommand(refit.step);
2283
2261
  }
2284
2262
  renderSingle(name, field, unavailable) {
2285
- /* A drawn section's point is called by its number and axis, as the section editor calls it (ADR-0100 §2.1, Q9). */
2286
- const point = /^point\.p(\d+)\.([xyz])$/.exec(name);
2287
- const apex = /^apex\.([xyz])$/.exec(name);
2288
- const words = point
2289
- ? [i18next.t('figure.label.section-editor-coordinate', { point: point[1], axis: point[2].toUpperCase() })]
2290
- : apex
2291
- ? [`${i18next.t('figure.label.section-editor-apex-mark')} ${apex[1].toUpperCase()}`]
2292
- : name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }));
2263
+ const words = name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }));
2293
2264
  return html `<span ?scrub=${!unavailable} @pointerdown=${(e) => this.scrub(field, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
2294
2265
  ><input
2295
2266
  aria-label=${field.id}