@things-factory/figure-ui 10.1.82 → 10.1.84

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 (83) hide show
  1. package/client/modeller/command-preview.ts +4 -2
  2. package/client/modeller/drawn-shapes.ts +29 -0
  3. package/client/modeller/editor.ts +11 -29
  4. package/client/modeller/figure-links.ts +47 -101
  5. package/client/modeller/figure-material-panel.ts +2 -3
  6. package/client/modeller/figure-parts.ts +9 -11
  7. package/client/modeller/figure-section-panel.ts +175 -0
  8. package/client/modeller/gizmo.ts +26 -16
  9. package/client/modeller/joint-fan.ts +4 -3
  10. package/client/modeller/links.ts +24 -24
  11. package/client/modeller/maturity.ts +12 -6
  12. package/client/modeller/motion-ends.ts +2 -1
  13. package/client/modeller/refusals.ts +20 -0
  14. package/client/modeller/renderer.ts +3 -2
  15. package/client/modeller/section-controls.ts +34 -0
  16. package/client/modeller/starting-shape.ts +34 -0
  17. package/dist-client/modeller/clips.d.ts +1 -1
  18. package/dist-client/modeller/command-preview.js +4 -2
  19. package/dist-client/modeller/command-preview.js.map +1 -1
  20. package/dist-client/modeller/drawn-shapes.d.ts +44 -0
  21. package/dist-client/modeller/drawn-shapes.js +12 -0
  22. package/dist-client/modeller/drawn-shapes.js.map +1 -0
  23. package/dist-client/modeller/editor.d.ts +1 -6
  24. package/dist-client/modeller/editor.js +10 -29
  25. package/dist-client/modeller/editor.js.map +1 -1
  26. package/dist-client/modeller/figure-links.d.ts +2 -2
  27. package/dist-client/modeller/figure-links.js +45 -96
  28. package/dist-client/modeller/figure-links.js.map +1 -1
  29. package/dist-client/modeller/figure-material-panel.js +2 -3
  30. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  31. package/dist-client/modeller/figure-parts.d.ts +3 -2
  32. package/dist-client/modeller/figure-parts.js +6 -9
  33. package/dist-client/modeller/figure-parts.js.map +1 -1
  34. package/dist-client/modeller/figure-section-panel.d.ts +28 -0
  35. package/dist-client/modeller/figure-section-panel.js +172 -0
  36. package/dist-client/modeller/figure-section-panel.js.map +1 -0
  37. package/dist-client/modeller/gizmo.d.ts +3 -3
  38. package/dist-client/modeller/gizmo.js +24 -11
  39. package/dist-client/modeller/gizmo.js.map +1 -1
  40. package/dist-client/modeller/joint-fan.js +4 -3
  41. package/dist-client/modeller/joint-fan.js.map +1 -1
  42. package/dist-client/modeller/links.d.ts +4 -1
  43. package/dist-client/modeller/links.js +23 -25
  44. package/dist-client/modeller/links.js.map +1 -1
  45. package/dist-client/modeller/maturity.d.ts +3 -2
  46. package/dist-client/modeller/maturity.js +9 -6
  47. package/dist-client/modeller/maturity.js.map +1 -1
  48. package/dist-client/modeller/motion-ends.js +2 -1
  49. package/dist-client/modeller/motion-ends.js.map +1 -1
  50. package/dist-client/modeller/refusals.d.ts +0 -5
  51. package/dist-client/modeller/refusals.js +25 -0
  52. package/dist-client/modeller/refusals.js.map +1 -1
  53. package/dist-client/modeller/renderer.d.ts +3 -4
  54. package/dist-client/modeller/renderer.js +3 -2
  55. package/dist-client/modeller/renderer.js.map +1 -1
  56. package/dist-client/modeller/section-controls.d.ts +11 -0
  57. package/dist-client/modeller/section-controls.js +27 -0
  58. package/dist-client/modeller/section-controls.js.map +1 -0
  59. package/dist-client/modeller/starting-shape.d.ts +32 -0
  60. package/dist-client/modeller/starting-shape.js +37 -0
  61. package/dist-client/modeller/starting-shape.js.map +1 -0
  62. package/dist-client/route.d.ts +1 -1
  63. package/dist-client/tsconfig.tsbuildinfo +1 -1
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +5 -5
  66. package/test/browser/acceptance-page.js +8 -0
  67. package/test/command-preview.test.ts +3 -2
  68. package/test/fixtures/joint-column.json +110 -15
  69. package/test/fixtures/joint-roller.json +66 -9
  70. package/test/fixtures/robot-arm-6dof.json +203 -27
  71. package/test/joint-fan.test.ts +10 -7
  72. package/test/links.test.ts +6 -5
  73. package/test/material.test.ts +7 -2
  74. package/test/renderer.test.ts +2 -1
  75. package/test/resize-with-placement.test.ts +15 -14
  76. package/test/scene-canvas.test.ts +2 -1
  77. package/test/section-panel.test.ts +93 -0
  78. package/translations/en.json +21 -2
  79. package/translations/ja.json +20 -1
  80. package/translations/ko.json +22 -3
  81. package/translations/ms.json +20 -1
  82. package/translations/zh.json +20 -1
  83. package/dist-client/tsconfig.localkernel.tsbuildinfo +0 -1
@@ -47,8 +47,8 @@ export declare class FigureLinks extends FigureLinks_base {
47
47
  private working;
48
48
  private fastenCandidate?;
49
49
  private rowCandidate?;
50
- /** The directions ticked for "follow the size, attached", per part, once the author has ticked or fixed any. */
51
- private pendingAxes;
50
+ /** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
51
+ private choosingFastening;
52
52
  /** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
53
53
  private spotlit;
54
54
  private fasten;
@@ -3,8 +3,8 @@ import '@material/web/icon/icon.js';
3
3
  import { css, html, LitElement, nothing } from 'lit';
4
4
  import { customElement, property, state } from 'lit/decorators.js';
5
5
  import { i18next, localize } from '@operato/i18n';
6
- import { FIGURE_FACES, planFigureResize } from '@hatiolab/figure-model';
7
- import { acceptsOf, alignAxesOf, mountingPlaneOf, alignOf, AXES, canFasten, facesOf, canFollow, dimensionNow, followableAxes, positionIsShare, choiceOf, heldBy, followOf, hangsFrom, shownWhenOf, visibilityControlsOf, placeKeptOf, fasteningOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, resizeAxesOf, fieldWordsOf, measureNow } from './links.js';
6
+ import { FIGURE_FACES } from '@hatiolab/figure-model';
7
+ import { acceptsOf, alignAxesOf, mountingPlaneOf, alignOf, AXES, canFasten, facesOf, canFollow, dimensionNow, followableAxes, positionIsShare, choiceOf, heldBy, followOf, hangsFrom, shownWhenOf, visibilityControlsOf, placeKeptOf, fasteningOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, fieldWordsOf, measureNow } from './links.js';
8
8
  import { SidePanelStyles } from './side-panel-styles.js';
9
9
  import { HelpToggles } from './help.js';
10
10
  const FACES = Object.keys(FIGURE_FACES);
@@ -58,8 +58,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
58
58
  this.told = '';
59
59
  /* Two forms describe a command here: the fastening and the row. The one being worked on is the one seen. */
60
60
  this.working = 'fasten';
61
- /** The directions ticked for "follow the size, attached", per part, once the author has ticked or fixed any. */
62
- this.pendingAxes = {};
61
+ /** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
62
+ this.choosingFastening = false;
63
63
  /** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
64
64
  this.spotlit = false;
65
65
  this.fasten = freshFasten();
@@ -91,10 +91,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
91
91
  div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
92
92
  div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
93
93
  button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
94
- /* What will be applied: a pending choice, boxed apart from the rows above. */
95
- fieldset[resize-pending] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border: 1px dashed var(--border-color, #8888); border-radius: 6px; }
96
- fieldset[resize-pending] label { display: flex; align-items: center; gap: 6px; }
97
- fieldset[resize-pending] small { opacity: 0.7; }
94
+ /* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
95
+ details[resize-choice] { margin: 12px 0; padding: 0; border: 0; }
96
+ details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
98
97
  div[resize-held] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
99
98
  div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
100
99
  p[fasten-mode] { margin: 0; font-weight: 600; }
@@ -131,6 +130,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
131
130
  this.place = freshPlace();
132
131
  this.row = freshRow();
133
132
  this.editingMotion = false;
133
+ this.choosingFastening = false;
134
134
  }
135
135
  }
136
136
  updated() {
@@ -245,39 +245,11 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
245
245
  this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capabilities', names } }, bubbles: true, composed: true }));
246
246
  }
247
247
  renderDimensions(asset) {
248
- /*
249
- The kernel's plan for "follow the size and keep where the figure is placed" (ADR-0097), with the directions the
250
- author ticked below -- a pending choice, drawn apart from the rows above, which only say what each direction does
251
- now (designer 2026-09-27: the immediate and the pending are not mixed in one look).
252
- */
253
248
  const editable = followableAxes(asset, this.partId);
254
- const choice = resizeAxesOf(asset, this.partId, this.pendingAxes[this.partId]);
255
- const axes = choice.axes;
256
- const plan = planFigureResize(asset, { part: this.partId, axes });
257
- const mode = plan.mode;
258
249
  const round = editable.length === 2;
259
250
  const word = (axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`);
260
- const scope = axes.length && axes.length === editable.length
261
- ? i18next.t(`figure.text.resize-scope-all-${editable.length}`)
262
- : i18next.t('figure.text.resize-scope-only', { axes: axes.map(word).join(' · ') });
263
- const ask = (gap) => this.dispatchEvent(new CustomEvent('resize-with-placement', { detail: { request: { part: this.partId, axes, ...(gap ? { gap } : {}) } }, bubbles: true, composed: true }));
264
- const gapChoice = plan.refusal?.code === 'GAP_CHOICE' ? plan.refusal : undefined;
265
- /* Brought onto the plane, where it moves -- said on the button before it is pressed. */
266
- const onto = gapChoice ? planFigureResize(asset, { part: this.partId, axes, gap: 'plane' }).moves[0] : undefined;
267
- const place = i18next.t(`figure.label.resize-place-${mode}`);
268
- const tick = (axis, on) => {
269
- const next = on ? [...new Set([...axes, axis])] : axes.filter(one => one !== axis);
270
- this.pendingAxes = { ...this.pendingAxes, [this.partId]: next };
271
- };
272
251
  const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one));
273
- /* One step for the whole part (figure-model follow-size): the size is declared from the parts if there is none. */
274
252
  const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance');
275
- const whole = followsAll
276
- ? nothing
277
- : html `<button act follow-size ?disabled=${this.locked} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
278
- <md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
279
- </button>`;
280
- const held = plan.refusal?.code === 'HELD';
281
253
  return html `<p hint>${i18next.t('figure.text.resize-intent')}</p>
282
254
  ${asset.sizing === 'uniform' ? html `<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
283
255
  <div resize-now>${editable.map(axis => {
@@ -292,46 +264,17 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
292
264
  ${following
293
265
  ? html `<button link resize-unfollow=${axis} ?disabled=${this.locked}
294
266
  @click=${() => {
295
- /* Set back to fixed: the author's choice for this part from now on, not a blank to fill with "all". */
296
- this.pendingAxes = { ...this.pendingAxes, [this.partId]: axes.filter(one => one !== axis) };
297
267
  this.follow(asset, axis, 'own', now);
298
268
  }}>${i18next.t('figure.button.resize-unfollow')}</button>`
299
269
  : nothing}
300
270
  </div>`;
301
271
  })}</div>
302
- ${held
303
- ? this.renderHeld(asset)
304
- : html `<fieldset resize-pending ?disabled=${this.locked || asset.sizing === 'uniform'}>
305
- <legend>${i18next.t('figure.label.resize-pending')}</legend>
306
- ${editable.map(axis => {
307
- const now = followOf(asset, this.partId, axis);
308
- const following = now.of === 'instance' && now.axis === axis;
309
- const custom = now.of !== 'own' && !following;
310
- return html `<label>
311
- <input type="checkbox" resize-tick=${axis} .checked=${axes.includes(axis)} ?disabled=${following || custom}
312
- @change=${(e) => tick(axis, e.target.checked)} />
313
- ${word(axis)}${following ? html ` <small>${i18next.t('figure.text.resize-already-following')}</small>` : nothing}
314
- </label>`;
315
- })}
316
- <p hint resize-basis>${i18next.t(`figure.text.resize-basis-${choice.basis}`)}</p>
317
- <p hint>${i18next.t('figure.text.placement-intent-' + mode)}</p>
318
- ${gapChoice
319
- ? html `<p hint gap-choice>${i18next.t(gapChoice.mm > 0 ? 'figure.text.resize-gap-off' : 'figure.text.resize-gap-through', { place, mm: String(Math.abs(Math.round(gapChoice.mm * 10) / 10)) })}</p>
320
- <button act resize-keep-gap @click=${() => ask('keep')}>
321
- ${i18next.t(gapChoice.mm > 0 ? 'figure.button.resize-keep-gap' : 'figure.button.resize-keep-through', { scope, mm: String(Math.abs(Math.round(gapChoice.mm * 10) / 10)) })}
322
- </button>
323
- <button act resize-onto-plane @click=${() => ask('plane')}>
324
- ${i18next.t('figure.button.resize-onto-plane-' + mode, { scope })}
325
- ${onto ? html `<small>${i18next.t('figure.text.resize-move', { axis: onto.axis.toUpperCase(), mm: String(Math.round((onto.to - onto.from) * 10) / 10) })}</small>` : nothing}
326
- </button>`
327
- : html `<button act resize-with-placement ?disabled=${choice.basis === 'none' || plan.refusal?.code === 'UNIFORM_POLICY'} @click=${() => ask()}>
328
- ${i18next.t('figure.button.resize-scoped-' + mode, { scope })}
329
- </button>`}
330
- ${!plan.refusal && plan.replaces.length
331
- ? html `<p hint resize-replaces>${i18next.t('figure.text.resize-replaces', { fields: [...new Set(plan.replaces.map(id => fieldWordsOf(id, key => i18next.t(key))))].join(' · '), place })}</p>`
332
- : nothing}
333
- </fieldset>`}
334
- <details><summary>${i18next.t('figure.label.resize-advanced')}</summary>${whole}${followableAxes(asset, this.partId).map(axis => {
272
+ ${followsAll
273
+ ? html `<p hint>${i18next.t('figure.text.resize-all-following')}</p>`
274
+ : html `<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
275
+ <md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
276
+ </button>`}
277
+ <details resize-choice><summary>${i18next.t('figure.label.resize-advanced')}</summary><div>${followableAxes(asset, this.partId).map(axis => {
335
278
  const now = followOf(asset, this.partId, axis);
336
279
  const value = choiceOf(now);
337
280
  /* A cylinder's two measures are its radius and its length; its X and Y name them. */
@@ -365,7 +308,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
365
308
  </label>`
366
309
  : nothing}
367
310
  </div>`;
368
- })}</details>`;
311
+ })}</div></details>`;
369
312
  }
370
313
  /**
371
314
  * "Open attachment settings": the fastening this part has now -- its face, what it is on, the gap -- in the form below,
@@ -543,6 +486,14 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
543
486
  target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
544
487
  })
545
488
  : i18next.t('figure.text.fasten-new');
489
+ /* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
490
+ if (!current && !this.choosingFastening && !f.target)
491
+ return html `
492
+ <p hint>${i18next.t('figure.text.fasten-start')}</p>
493
+ <button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
494
+ <md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
495
+ </button>
496
+ `;
546
497
  return html `
547
498
  <p fasten-mode ?editing=${!!current}>${editing}</p>
548
499
  ${this.overhangNote.length ? html `<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
@@ -563,32 +514,30 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
563
514
  </div>
564
515
  ${this.renderDetachWords()}`
565
516
  : nothing}
566
- <div form>
567
- <span>${i18next.t('figure.label.my-face')}</span>
568
- <select @change=${(e) => (this.fasten = { ...f, face: e.target.value })}>
569
- ${mineFaces.map(face => html `<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
570
- </select>
571
- </div>
572
- <label toggle>
573
- <input type="checkbox" .checked=${f.toPlane} @change=${(e) => this.aimAt(asset, { ...f, toPlane: e.target.checked })} />
574
- ${i18next.t('figure.label.stands-on-plane')}
575
- </label>
576
517
  ${f.toPlane
577
- ? nothing
518
+ ? html `<p hint>${i18next.t('figure.text.fasten-edit-on-plane')}</p>`
578
519
  : html `<div form>
579
- <span>${i18next.t('figure.label.target-part')}</span>
580
- <select @change=${(e) => this.aimAt(asset, { ...f, target: e.target.value })}>
581
- <option value="" ?selected=${!f.target}>—</option>
582
- ${targets.map(one => loops(one)
520
+ <span>${i18next.t('figure.label.target-part')}</span>
521
+ <select @change=${(e) => this.aimAt(asset, { ...f, target: e.target.value })}>
522
+ <option value="" ?selected=${!f.target}>${i18next.t('figure.label.target-part-placeholder')}</option>
523
+ ${targets.map(one => loops(one)
583
524
  ? html `<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
584
525
  : html `<option value=${one} ?selected=${one === f.target}>${one}</option>`)}
585
- </select>
586
- <span>${i18next.t('figure.label.target-face')}</span>
587
- <select @change=${(e) => this.aimAt(asset, { ...f, targetFace: e.target.value })}>
588
- ${theirFaces.map(face => html `<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
589
- </select>
590
- </div>`}
591
- <details fasten-detail>
526
+ </select>
527
+ </div>
528
+ ${f.target
529
+ ? html `<div form>
530
+ <span>${i18next.t('figure.label.my-face')}</span>
531
+ <select @change=${(e) => (this.fasten = { ...f, face: e.target.value })}>
532
+ ${mineFaces.map(face => html `<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
533
+ </select>
534
+ <span>${i18next.t('figure.label.target-face')}</span>
535
+ <select @change=${(e) => this.aimAt(asset, { ...f, targetFace: e.target.value })}>
536
+ ${theirFaces.map(face => html `<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
537
+ </select>
538
+ </div>`
539
+ : html `<p hint>${i18next.t('figure.text.fasten-target-first')}</p>`}`}
540
+ ${(f.toPlane || f.target) ? html `<details fasten-detail>
592
541
  <summary>${i18next.t('figure.label.fasten-detail')} · <span fasten-summary>${detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')}</span></summary>
593
542
  ${f.toPlane
594
543
  ? nothing
@@ -610,7 +559,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
610
559
  ${action && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
611
560
  <button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
612
561
  <md-icon>link</md-icon>${i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')}
613
- </button>
562
+ </button>` : nothing}
614
563
  `;
615
564
  }
616
565
  renderDetachWords() {
@@ -937,7 +886,7 @@ __decorate([
937
886
  __decorate([
938
887
  state(),
939
888
  __metadata("design:type", Object)
940
- ], FigureLinks.prototype, "pendingAxes", void 0);
889
+ ], FigureLinks.prototype, "choosingFastening", void 0);
941
890
  __decorate([
942
891
  state(),
943
892
  __metadata("design:type", Object)