@things-factory/figure-ui 10.1.169 → 10.1.170

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 (105) hide show
  1. package/client/modeller/affected-parts.ts +11 -0
  2. package/client/modeller/canvas.ts +15 -1
  3. package/client/modeller/editor-styles.ts +98 -6
  4. package/client/modeller/editor.ts +212 -53
  5. package/client/modeller/fasten-geometry.ts +19 -0
  6. package/client/modeller/figure-fasten-drawing.ts +4 -3
  7. package/client/modeller/figure-links.ts +80 -37
  8. package/client/modeller/figure-material-panel.ts +20 -1
  9. package/client/modeller/figure-preview.ts +117 -8
  10. package/client/modeller/figure-properties.ts +1 -2
  11. package/client/modeller/figure-section-panel.ts +5 -0
  12. package/client/modeller/figure-verdict.ts +9 -2
  13. package/client/modeller/gate-findings.ts +12 -4
  14. package/client/modeller/group-menu.ts +57 -0
  15. package/client/modeller/links.ts +22 -1
  16. package/client/modeller/modeller-keys.ts +6 -0
  17. package/client/modeller/preview-size.ts +32 -0
  18. package/client/modeller/stage-model.ts +11 -0
  19. package/client/pages/figure-modeller-page.ts +8 -2
  20. package/dist-client/modeller/affected-parts.d.ts +6 -0
  21. package/dist-client/modeller/affected-parts.js +12 -0
  22. package/dist-client/modeller/affected-parts.js.map +1 -0
  23. package/dist-client/modeller/canvas.d.ts +3 -0
  24. package/dist-client/modeller/canvas.js +12 -1
  25. package/dist-client/modeller/canvas.js.map +1 -1
  26. package/dist-client/modeller/clips.d.ts +1 -1
  27. package/dist-client/modeller/editor-styles.js +98 -6
  28. package/dist-client/modeller/editor-styles.js.map +1 -1
  29. package/dist-client/modeller/editor.d.ts +42 -1
  30. package/dist-client/modeller/editor.js +220 -51
  31. package/dist-client/modeller/editor.js.map +1 -1
  32. package/dist-client/modeller/fasten-geometry.d.ts +27 -0
  33. package/dist-client/modeller/fasten-geometry.js +14 -0
  34. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  35. package/dist-client/modeller/figure-fasten-drawing.js +4 -3
  36. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  37. package/dist-client/modeller/figure-links.d.ts +3 -1
  38. package/dist-client/modeller/figure-links.js +84 -38
  39. package/dist-client/modeller/figure-links.js.map +1 -1
  40. package/dist-client/modeller/figure-material-panel.d.ts +8 -1
  41. package/dist-client/modeller/figure-material-panel.js +24 -1
  42. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  43. package/dist-client/modeller/figure-preview.d.ts +12 -0
  44. package/dist-client/modeller/figure-preview.js +116 -5
  45. package/dist-client/modeller/figure-preview.js.map +1 -1
  46. package/dist-client/modeller/figure-properties.js +1 -2
  47. package/dist-client/modeller/figure-properties.js.map +1 -1
  48. package/dist-client/modeller/figure-section-panel.d.ts +1 -1
  49. package/dist-client/modeller/figure-section-panel.js +5 -0
  50. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  51. package/dist-client/modeller/figure-verdict.d.ts +1 -0
  52. package/dist-client/modeller/figure-verdict.js +8 -2
  53. package/dist-client/modeller/figure-verdict.js.map +1 -1
  54. package/dist-client/modeller/gate-findings.d.ts +3 -1
  55. package/dist-client/modeller/gate-findings.js +8 -4
  56. package/dist-client/modeller/gate-findings.js.map +1 -1
  57. package/dist-client/modeller/group-menu.d.ts +18 -0
  58. package/dist-client/modeller/group-menu.js +52 -0
  59. package/dist-client/modeller/group-menu.js.map +1 -0
  60. package/dist-client/modeller/links.d.ts +5 -0
  61. package/dist-client/modeller/links.js +21 -1
  62. package/dist-client/modeller/links.js.map +1 -1
  63. package/dist-client/modeller/modeller-keys.js +7 -0
  64. package/dist-client/modeller/modeller-keys.js.map +1 -1
  65. package/dist-client/modeller/preview-size.d.ts +23 -0
  66. package/dist-client/modeller/preview-size.js +32 -0
  67. package/dist-client/modeller/preview-size.js.map +1 -0
  68. package/dist-client/modeller/stage-model.d.ts +11 -0
  69. package/dist-client/modeller/stage-model.js +11 -0
  70. package/dist-client/modeller/stage-model.js.map +1 -1
  71. package/dist-client/pages/figure-modeller-page.d.ts +3 -0
  72. package/dist-client/pages/figure-modeller-page.js +11 -2
  73. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  74. package/dist-client/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +8 -8
  76. package/test/browser/editing-page.js +3 -1
  77. package/test/browser/editing.check.mjs +2 -1
  78. package/test/browser/links-page.js +1 -0
  79. package/test/browser/links.check.mjs +20 -5
  80. package/test/browser/modeller.check.mjs +6 -0
  81. package/test/browser/more-open.js +11 -0
  82. package/test/browser/parts.check.mjs +11 -1
  83. package/test/browser/places-mesh.check.mjs +9 -3
  84. package/test/browser/product-page.check.mjs +9 -4
  85. package/test/browser/props.check.mjs +224 -0
  86. package/test/browser/screen-page.js +17 -0
  87. package/test/browser/screen.check.mjs +171 -29
  88. package/test/browser/section-page.js +1 -0
  89. package/test/browser/section.check.mjs +6 -0
  90. package/test/command-preview.test.ts +2 -2
  91. package/test/fasten-geometry.test.ts +15 -0
  92. package/test/fields.test.ts +4 -2
  93. package/test/first-minute.test.ts +15 -2
  94. package/test/layout.test.ts +1 -1
  95. package/test/preview-size.test.ts +33 -0
  96. package/test/refusals.test.ts +1 -1
  97. package/test/resize-with-placement.test.ts +1 -1
  98. package/test/stage-model.test.ts +11 -1
  99. package/test/verdict.test.ts +7 -0
  100. package/translations/en.json +37 -27
  101. package/translations/es.json +18 -8
  102. package/translations/ja.json +19 -9
  103. package/translations/ko.json +50 -40
  104. package/translations/ms.json +19 -9
  105. package/translations/zh.json +19 -9
@@ -6,11 +6,12 @@ import './figure-slot-layout-panel.js';
6
6
  import { customElement, property, state } from 'lit/decorators.js';
7
7
  import { i18next, localize } from '@operato/i18n';
8
8
  import { FIGURE_FACES, figureFaceOverhangOf } from '@hatiolab/figure-model';
9
- import { acceptsOf, alignOf, AXES, canFasten, facesOf, canFollow, followCommandOf, followableAxes, positionIsShare, choiceOf, heldBy, followOf, hangsFrom, shownWhenOf, visibilityControlsOf, placeKeptOf, fasteningOf, fastenGapOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionFormRefusalOf, endPlaceKeyOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, fieldWordsOf, measureNow, ownFacesOf, alignRowsOf } from './links.js';
9
+ import { acceptsOf, alignOf, AXES, canFasten, facesOf, canFollow, followCommandOf, followableAxes, positionIsShare, choiceOf, followWordsOf, heldBy, followOf, hangsFrom, shownWhenOf, visibilityControlsOf, placeKeptOf, fasteningOf, fastenGapOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionFormRefusalOf, endPlaceKeyOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, fieldWordsOf, measureNow, ownFacesOf, alignRowsOf } from './links.js';
10
10
  import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
11
11
  import './figure-fasten-drawing.js';
12
12
  import { faceViewOf, insideAlignmentOf, modeOf, myFaceFor, turnsInPlace } from './fasten-geometry.js';
13
13
  import { HelpToggles } from './help.js';
14
+ import { groupMenu, GroupMenuStyles } from './group-menu.js';
14
15
  import { jointAxisWord } from './joint-axis.js';
15
16
  import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js';
16
17
  const FACES = Object.keys(FIGURE_FACES);
@@ -69,6 +70,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
69
70
  this.working = 'fasten';
70
71
  /** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
71
72
  this.choosingFastening = false;
73
+ /** The fastening the part has, opened to be changed: until then the place group says it in one line. */
74
+ this.changingFastening = false;
72
75
  /** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
73
76
  this.spotlit = false;
74
77
  this.fasten = freshFasten();
@@ -80,10 +83,28 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
80
83
  /** The motion form is open over the part's own motion, to change it where it is. */
81
84
  this.editingMotion = false;
82
85
  }
83
- static { this.styles = [SidePanelStyles, css `
86
+ static { this.styles = [SidePanelStyles, GroupMenuStyles, css `
84
87
  :host {
85
88
  display: block;
86
89
  }
90
+ /* A group's sub-question, under its title: quieter than the title, as a label is. */
91
+ p[group-question] {
92
+ margin: 4px 0 0;
93
+ color: var(--md-sys-color-on-surface-variant);
94
+ }
95
+ /* The fastening a part has, in one line, and 「바꾸기」 at its end. */
96
+ div[fastened-summary] {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 8px;
100
+ }
101
+ div[fastened-summary] > p {
102
+ flex: 1;
103
+ margin: 0;
104
+ }
105
+ div[fastened-summary] > button[quiet] {
106
+ margin-left: 0;
107
+ }
87
108
  /* The measure's name, then its rule: X, or a cylinder's 반지름 · 길이, on one line. */
88
109
  div[row] > b {
89
110
  white-space: nowrap;
@@ -101,9 +122,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
101
122
  div[resize-now] { display: grid; gap: 4px; margin: 12px 0; }
102
123
  div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
103
124
  div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
104
- button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
105
125
  /* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
106
- details[resize-choice] { margin: 12px 0; padding: 0; border: 0; }
126
+ /* One of the size group's rows: the group's gap spaces it, as it does the rest (a margin on top of it left a hole). */
127
+ details[resize-choice] { margin: 0; padding: 0; border: 0; }
107
128
  details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
108
129
  p[fasten-mode] { margin: 0; font-weight: 600; }
109
130
  section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
@@ -149,6 +170,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
149
170
  this.row = freshRow();
150
171
  this.editingMotion = false;
151
172
  this.choosingFastening = false;
173
+ this.changingFastening = false;
152
174
  }
153
175
  }
154
176
  updated() {
@@ -201,17 +223,29 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
201
223
  ? this.renderPlaces(asset)
202
224
  : html `<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`;
203
225
  }
226
+ /*
227
+ Three groups in one order whatever the part's state (chief architect 2026-10-16): its size, its place, its row. The
228
+ editor's own numbers go in through the slots, so a group keeps its place when a rule or a fastening takes them over.
229
+ */
230
+ /* Fastened, by a face or by an origin: either can be taken off, from the place group's 「⋯」. */
231
+ const held = !!heldBy(asset, this.partId) || !!(this.revision && fasteningOf(this.revision, this.partId)?.atOrigin);
204
232
  const shown = html `
205
- <section>
206
- <h3>${i18next.t('figure.label.resize-intent')}</h3>
233
+ <section size>
234
+ <h3>${i18next.t('figure.label.group-size')}</h3>
235
+ <slot name="size"></slot>
236
+ <p group-question>${i18next.t('figure.label.resize-intent')}</p>
207
237
  ${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html `<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
208
238
  </section>
209
239
  <section fastening ?spotlit=${this.spotlit} @focusin=${() => (this.working = 'fasten')}>
210
- <h3>${i18next.t('figure.label.placement')}${this.help.button('figure.text.fastening-explained')}</h3>${this.help.hint('figure.text.fastening-explained')}
240
+ <h3>
241
+ ${i18next.t('figure.label.group-place')}${this.help.button('figure.text.fastening-explained')}
242
+ ${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
243
+ </h3>${this.help.hint('figure.text.fastening-explained')}
244
+ <slot name="place"></slot>
211
245
  ${this.renderPositions(asset)}
212
246
  ${canFasten(asset, this.partId) ? this.renderFastening(asset) : html `<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
213
247
  </section>
214
- ${this.renderRow(asset)}
248
+ ${rowOf(asset, this.partId) ? html `<slot name="row"></slot>` : this.renderRow(asset)}
215
249
  `;
216
250
  this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate;
217
251
  return shown;
@@ -230,7 +264,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
230
264
  <b bound>${one.label}</b>
231
265
  <small>${one.face ? i18next.t('figure.label.face-' + one.face) + ' · ' : ''}${placeTerms(one, (key, params) => i18next.t(key, params))}</small>
232
266
  </span>
233
- <button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-binding', id: one.id })}>
267
+ <button quiet ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-binding', id: one.id })}>
234
268
  <md-icon>close</md-icon>${i18next.t('figure.button.remove-place')}
235
269
  </button>
236
270
  </div>`)}
@@ -298,9 +332,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
298
332
  return html `<div row>
299
333
  <b>${word(axis)}</b>
300
334
  <span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
301
- <span>${i18next.t(custom ? 'figure.label.resize-custom' : following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
335
+ <span>${custom ? followWordsOf(now, (key, params) => i18next.t(key, params)) : i18next.t(following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
302
336
  ${following
303
- ? html `<button link resize-unfollow=${axis} ?disabled=${this.locked}
337
+ ? html `<button quiet resize-unfollow=${axis} ?disabled=${this.locked}
304
338
  @click=${() => {
305
339
  this.follow(axis, 'own', now);
306
340
  }}>${i18next.t('figure.button.resize-unfollow')}</button>`
@@ -308,7 +342,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
308
342
  </div>`;
309
343
  })}</div>`}
310
344
  ${followsAll
311
- ? html `<p hint>${i18next.t('figure.text.resize-all-following')}</p>`
345
+ ? nothing
312
346
  : html `<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
313
347
  <md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
314
348
  </button>`}
@@ -322,12 +356,12 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
322
356
  <select ?disabled=${this.locked} @change=${(e) => this.follow(axis, e.target.value, now)}>
323
357
  <option value="own" ?selected=${value === 'own'}>${i18next.t('figure.label.follows-nothing')}</option>
324
358
  ${AXES.filter(one => hasInstanceSize(asset, one)).map(one => html `<option value=${`share:${one}`} ?selected=${value === `share:${one}`}>
325
- ${i18next.t('figure.label.follows-instance-share')} · ${one.toUpperCase()}
359
+ ${i18next.t('figure.label.follows-instance-share', { axis: one.toUpperCase() })}
326
360
  </option>
327
361
  <option value=${`instance:${one}`} ?selected=${value === `instance:${one}`}>
328
- ${i18next.t('figure.label.follows-instance')} · ${one.toUpperCase()}
362
+ ${i18next.t('figure.label.follows-instance', { axis: one.toUpperCase() })}
329
363
  </option>`)}
330
- ${others.flatMap(other => AXES.map(one => html `<option value=${`part:${other}:${one}`} ?selected=${value === `part:${other}:${one}`}>${other} · ${one.toUpperCase()}</option>`))}
364
+ ${others.flatMap(other => AXES.map(one => html `<option value=${`part:${other}:${one}`} ?selected=${value === `part:${other}:${one}`}>${i18next.t('figure.label.follows-part-axis', { part: other, axis: one.toUpperCase() })}</option>`))}
331
365
  ${now.of === 'expression' || now.of === 'input'
332
366
  ? html `<option value=${value} selected>${now.of === 'input' ? now.input : i18next.t('figure.label.follows-expression')}</option>`
333
367
  : nothing}
@@ -384,6 +418,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
384
418
  async openFastening(asset) {
385
419
  const own = fasteningOf(this.revision, this.partId);
386
420
  this.fasten = own ? { ...freshFasten(), ...own } : freshFasten();
421
+ /* Asked for from elsewhere: the form itself, not the one-line summary of the fastening it has. */
422
+ this.changingFastening = true;
387
423
  this.spotlit = true;
388
424
  await this.updateComplete;
389
425
  const section = this.renderRoot.querySelector('section[fastening]');
@@ -458,9 +494,6 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
458
494
  this.fastenCandidate = undefined;
459
495
  return html `<div fastened fasten-at-origin>
460
496
  <p>${this.heldWords(asset)}</p>
461
- <button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
462
- <md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
463
- </button>
464
497
  ${this.renderDetachWords()}
465
498
  </div>`;
466
499
  }
@@ -520,17 +553,13 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
520
553
  */
521
554
  const current = asset.joints?.find(j => j.body1.part === this.partId);
522
555
  const editing = i18next.t('figure.text.fasten-new');
523
- /* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
524
- if (!current && !this.choosingFastening && !f.target)
525
- return html `
526
- <p hint>${i18next.t('figure.text.fasten-start')}</p>
527
- <button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
528
- <md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
529
- </button>
530
- `;
531
- return html `
532
- ${current ? nothing : html `<p fasten-mode>${editing}</p>`}
533
- ${this.overhangNote.length ? html `<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
556
+ /*
557
+ A fastening the part has, said in one line -- what, on which face, how -- and 「바꾸기」 to open the drawing and the
558
+ details (chief architect 2026-10-16). They were open on every fastened part, near a screen of the panel, for a
559
+ choice made once. Taking it off is in the group's 「⋯」. What would hang over still shows: it is about now.
560
+ */
561
+ /* What hangs over the face, and the way to fix it: about the fastening as it is now, so said open or folded. */
562
+ const overhang = html `${this.overhangNote.length ? html `<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
534
563
  ${this.overhangWords.length
535
564
  ? html `<div overhang role="status">
536
565
  ${this.overhangWords.map(line => html `<p>${line}</p>`)}
@@ -541,15 +570,28 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
541
570
  ${this.fixNote ? html `<p hint fix-note>${this.fixNote}</p>` : nothing}`
542
571
  : nothing}
543
572
  </div>`
544
- : nothing}
545
- ${held
546
- ? html `<div fastened>
547
- <button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
548
- <md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
549
- </button>
550
- ${this.renderDetachWords()}
551
- </div>`
552
- : nothing}
573
+ : nothing}`;
574
+ if (current && held && !this.changingFastening && (f.toPlane || f.target))
575
+ return html `
576
+ ${overhang}
577
+ <div fastened-summary>
578
+ <p fasten-sentence>${this.sentenceOf(myFace, f.toPlane ? planeWord : `${f.target}의 ${i18next.t('figure.label.face-' + targetFace)}`, mode)}</p>
579
+ <button quiet change-fastening ?disabled=${this.locked} @click=${() => (this.changingFastening = true)}>${i18next.t('figure.button.change-fastening')}</button>
580
+ </div>
581
+ ${this.renderDetachWords()}
582
+ `;
583
+ /* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
584
+ if (!current && !this.choosingFastening && !f.target)
585
+ return html `
586
+ <p hint>${i18next.t('figure.text.fasten-start')}</p>
587
+ <button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
588
+ <md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
589
+ </button>
590
+ `;
591
+ return html `
592
+ ${current ? nothing : html `<p fasten-mode>${editing}</p>`}
593
+ ${overhang}
594
+ ${held ? this.renderDetachWords() : nothing}
553
595
  <div form>
554
596
  <span>${i18next.t('figure.label.target-part')}</span>
555
597
  <select @change=${(e) => {
@@ -989,6 +1031,10 @@ __decorate([
989
1031
  state(),
990
1032
  __metadata("design:type", Object)
991
1033
  ], FigureLinks.prototype, "choosingFastening", void 0);
1034
+ __decorate([
1035
+ state(),
1036
+ __metadata("design:type", Object)
1037
+ ], FigureLinks.prototype, "changingFastening", void 0);
992
1038
  __decorate([
993
1039
  state(),
994
1040
  __metadata("design:type", Object)