@things-factory/figure-ui 10.1.91 → 10.1.92

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 (30) hide show
  1. package/client/modeller/editor.ts +61 -90
  2. package/client/modeller/fields.ts +8 -1
  3. package/client/modeller/figure-section-close-choice.ts +52 -0
  4. package/client/modeller/figure-section-editor.ts +58 -3
  5. package/client/modeller/figure-section-panel.ts +3 -0
  6. package/client/modeller/side-panel-styles.ts +4 -11
  7. package/dist-client/modeller/editor.d.ts +10 -4
  8. package/dist-client/modeller/editor.js +59 -88
  9. package/dist-client/modeller/editor.js.map +1 -1
  10. package/dist-client/modeller/fields.js +8 -1
  11. package/dist-client/modeller/fields.js.map +1 -1
  12. package/dist-client/modeller/figure-section-close-choice.d.ts +17 -0
  13. package/dist-client/modeller/figure-section-close-choice.js +58 -0
  14. package/dist-client/modeller/figure-section-close-choice.js.map +1 -0
  15. package/dist-client/modeller/figure-section-editor.d.ts +11 -1
  16. package/dist-client/modeller/figure-section-editor.js +57 -3
  17. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  18. package/dist-client/modeller/figure-section-panel.js +3 -0
  19. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  20. package/dist-client/modeller/side-panel-styles.js +4 -11
  21. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  22. package/dist-client/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +2 -2
  24. package/test/layout.test.ts +19 -4
  25. package/test/section-editor.test.ts +31 -8
  26. package/translations/en.json +19 -15
  27. package/translations/ja.json +15 -11
  28. package/translations/ko.json +19 -15
  29. package/translations/ms.json +15 -11
  30. package/translations/zh.json +15 -11
@@ -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}