@things-factory/figure-ui 10.1.82 → 10.1.83
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.
- package/client/modeller/editor.ts +3 -28
- package/client/modeller/figure-links.ts +47 -101
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/editor.d.ts +0 -6
- package/dist-client/modeller/editor.js +2 -28
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +1 -1
- package/dist-client/modeller/figure-clips.d.ts +1 -1
- package/dist-client/modeller/figure-controls.d.ts +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -3
- package/dist-client/modeller/figure-links.js +45 -96
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +1 -1
- package/dist-client/modeller/figure-properties.d.ts +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/test/links.test.ts +2 -1
- package/test/resize-with-placement.test.ts +12 -11
- package/translations/en.json +8 -2
- package/translations/ja.json +7 -1
- package/translations/ko.json +9 -3
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
|
@@ -15,7 +15,7 @@ export declare class FigureCapabilityPanel extends FigureCapabilityPanel_base {
|
|
|
15
15
|
/** The selected part, whose places are marked. */
|
|
16
16
|
selected: string;
|
|
17
17
|
locked: boolean;
|
|
18
|
-
render(): import("lit-html").TemplateResult<1
|
|
18
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
19
19
|
private setRung;
|
|
20
20
|
private pick;
|
|
21
21
|
}
|
|
@@ -18,7 +18,7 @@ export declare class FigureClips extends FigureClips_base {
|
|
|
18
18
|
/** Where each clip is scrubbed to, in seconds. */
|
|
19
19
|
private at;
|
|
20
20
|
private adding;
|
|
21
|
-
render(): import("lit-html").TemplateResult<1
|
|
21
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
22
22
|
private renderClip;
|
|
23
23
|
private renderTrack;
|
|
24
24
|
private renderAdd;
|
|
@@ -22,7 +22,7 @@ export declare class FigureControls extends FigureControls_base {
|
|
|
22
22
|
private open;
|
|
23
23
|
/** Values in hand while a slider is dragged: the dial and the number follow them before the release commits. */
|
|
24
24
|
private live;
|
|
25
|
-
render(): import("lit-html").TemplateResult<1
|
|
25
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
26
26
|
private renderControl;
|
|
27
27
|
private renderDial;
|
|
28
28
|
private renderSettings;
|
|
@@ -47,8 +47,8 @@ export declare class FigureLinks extends FigureLinks_base {
|
|
|
47
47
|
private working;
|
|
48
48
|
private fastenCandidate?;
|
|
49
49
|
private rowCandidate?;
|
|
50
|
-
/**
|
|
51
|
-
private
|
|
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;
|
|
@@ -61,7 +61,7 @@ export declare class FigureLinks extends FigureLinks_base {
|
|
|
61
61
|
willUpdate(_changed: Map<string, unknown>): void;
|
|
62
62
|
updated(): void;
|
|
63
63
|
disconnectedCallback(): void;
|
|
64
|
-
render(): import("lit-html").TemplateResult<1
|
|
64
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
65
65
|
/** Where things are held on this part: a seat things rest on, or a grip things are held in. */
|
|
66
66
|
private renderPlaces;
|
|
67
67
|
/** The figure holds things: `Capacity` and `Holdable`, the ladder's middle rung. One part edit, one undo step. */
|
|
@@ -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
|
|
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,
|
|
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
|
-
/**
|
|
62
|
-
this.
|
|
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
|
-
/*
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
${
|
|
303
|
-
?
|
|
304
|
-
: html `<
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
?
|
|
518
|
+
? html `<p hint>${i18next.t('figure.text.fasten-edit-on-plane')}</p>`
|
|
578
519
|
: html `<div form>
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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, "
|
|
889
|
+
], FigureLinks.prototype, "choosingFastening", void 0);
|
|
941
890
|
__decorate([
|
|
942
891
|
state(),
|
|
943
892
|
__metadata("design:type", Object)
|