@things-factory/figure-ui 10.1.204 → 10.1.205
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/canvas.ts +91 -9
- package/client/modeller/editor-styles.ts +7 -22
- package/client/modeller/editor.ts +176 -145
- package/client/modeller/fasten-form.ts +85 -6
- package/client/modeller/figure-fasten-dialog.ts +42 -9
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +12 -1
- package/client/modeller/links.ts +48 -6
- package/client/modeller/section-drawing.ts +7 -7
- package/client/modeller/size-drag.ts +112 -0
- package/client/modeller/slot-layout-host.ts +3 -3
- package/client/modeller/stage-model.ts +6 -9
- package/client/pages/figure-modeller-page.ts +1 -4
- package/dist-client/modeller/canvas.d.ts +24 -3
- package/dist-client/modeller/canvas.js +96 -11
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +7 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +28 -32
- package/dist-client/modeller/editor.js +176 -153
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +36 -3
- package/dist-client/modeller/fasten-form.js +80 -5
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +17 -3
- package/dist-client/modeller/figure-fasten-dialog.js +47 -10
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
- package/dist-client/modeller/figure-fasten-drawing.js +235 -11
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +3 -3
- package/dist-client/modeller/figure-section-editor.js +4 -4
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +12 -1
- package/dist-client/modeller/gizmo.js +8 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +14 -2
- package/dist-client/modeller/links.js +42 -4
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +3 -4
- package/dist-client/modeller/section-drawing.js +10 -9
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/size-drag.d.ts +38 -0
- package/dist-client/modeller/size-drag.js +85 -0
- package/dist-client/modeller/size-drag.js.map +1 -0
- package/dist-client/modeller/slot-layout-host.d.ts +1 -3
- package/dist-client/modeller/slot-layout-host.js +3 -3
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +5 -7
- package/dist-client/modeller/stage-model.js +6 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -2
- package/dist-client/pages/figure-modeller-page.js +1 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +7 -11
- package/test/browser/acceptance-page.js +10 -0
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +94 -56
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +32 -10
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +2 -2
- package/test/fasten-form.test.ts +62 -1
- package/test/gizmo.test.ts +13 -15
- package/test/links.test.ts +8 -3
- package/test/resize-with-placement.test.ts +14 -13
- package/test/revision.test.ts +3 -2
- package/test/section-editor.test.ts +9 -10
- package/test/size-drag.test.ts +114 -0
- package/test/slot-layout-host.test.ts +13 -8
- package/translations/en.json +25 -22
- package/translations/es.json +25 -22
- package/translations/ja.json +25 -22
- package/translations/ko.json +25 -22
- package/translations/ms.json +25 -22
- package/translations/zh.json +25 -22
|
@@ -7,7 +7,7 @@ import { openPopup } from '@operato/layout';
|
|
|
7
7
|
import { OxPrompt } from '@operato/popup';
|
|
8
8
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
9
9
|
import { live } from 'lit/directives/live.js';
|
|
10
|
-
import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor,
|
|
10
|
+
import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
|
|
11
11
|
import './figure-recipe-catalog.js';
|
|
12
12
|
import { captureFigureThumbnail } from './thumbnail.js';
|
|
13
13
|
import { drawableFigureParts } from './renderer.js';
|
|
@@ -28,7 +28,8 @@ import './figure-preview.js';
|
|
|
28
28
|
import './figure-settings.js';
|
|
29
29
|
import { DEFAULT_VIEW } from '@things-factory/figure-viewer/dist-client/figure-view.js';
|
|
30
30
|
import './figure-capability-panel.js';
|
|
31
|
-
import { gizmoChanges, gizmoReachOf } from './gizmo.js';
|
|
31
|
+
import { gizmoChanges, gizmoReachOf, sizeKeepOf } from './gizmo.js';
|
|
32
|
+
import { keptBySizeOf, partsAtFrame, SIZE_KEEPS_WORDS, startSizeDrag, throughPlaneOf } from './size-drag.js';
|
|
32
33
|
import { ModellerKeys } from './modeller-keys.js';
|
|
33
34
|
import { PaneFold } from './pane-fold.js';
|
|
34
35
|
import { PanelMemory } from './panel-memory.js';
|
|
@@ -50,7 +51,8 @@ import { nudgeOf, placementOf } from './stage-model.js';
|
|
|
50
51
|
import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js';
|
|
51
52
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js';
|
|
52
53
|
import { editRefusalOf, inDeclaredRoom, narrowingRetryOf, partNameRefusalOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js';
|
|
53
|
-
import {
|
|
54
|
+
import { HelpToggles } from './help.js';
|
|
55
|
+
import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
|
|
54
56
|
import { jointFanOf } from './joint-fan.js';
|
|
55
57
|
import { derivedOf } from './revision.js';
|
|
56
58
|
import { jointDragOf } from './joint-drag.js';
|
|
@@ -76,14 +78,16 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
76
78
|
/** Why the last recipe was refused, in the kernel's words. */
|
|
77
79
|
this.recipeError = '';
|
|
78
80
|
this.revision = 0;
|
|
81
|
+
/** The 「?」 beside a group's title and the sentence it opens. */
|
|
82
|
+
this.help = new HelpToggles(this);
|
|
79
83
|
this.selected = '';
|
|
80
84
|
this.error = '';
|
|
81
85
|
this.tab = 'properties';
|
|
82
86
|
/**
|
|
83
|
-
* The fastening window open for the selected part (user 2026-10-17)
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
+
* The fastening window open for the selected part (user 2026-10-17), in the standard popup, moved by its head
|
|
88
|
+
* (2026-10-18; put back 2026-10-20 -- the side panel it was moved to could not be moved), with no backdrop, so the
|
|
89
|
+
* canvas can be pressed while it is open: chosen there, applied once. It closes with nothing written when the part,
|
|
90
|
+
* the tab or the middle changes under it.
|
|
87
91
|
*/
|
|
88
92
|
this.fastening = false;
|
|
89
93
|
/** Why the window's 「적용」 was refused, said in it. */
|
|
@@ -145,7 +149,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
145
149
|
this.panelMemory = new PanelMemory(() => this.renderRoot);
|
|
146
150
|
/** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
|
|
147
151
|
this.previewMounted = false;
|
|
148
|
-
this.lastAnchors = {};
|
|
149
152
|
/** Data tried in the data tab, given to the preview's figures as their `data`; null when none is (ADR-0051 §8). */
|
|
150
153
|
this.dataSample = null;
|
|
151
154
|
/*
|
|
@@ -221,7 +224,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
221
224
|
aria-pressed=${String(this.gizmoMode === tool.mode)}
|
|
222
225
|
?disabled=${!tool.can}
|
|
223
226
|
aria-label=${i18next.t('figure.button.gizmo-' + tool.mode)}
|
|
224
|
-
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' +
|
|
227
|
+
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t('figure.text.gizmo-not-own')}`}
|
|
225
228
|
@click=${() => this.useGizmo(tool.mode)}
|
|
226
229
|
>
|
|
227
230
|
<md-icon>${tool.icon}</md-icon>
|
|
@@ -263,7 +266,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
263
266
|
if (_changed.has('selected'))
|
|
264
267
|
this.gizmoMode = undefined;
|
|
265
268
|
/* The window is about the part chosen on the properties tab, being edited: it does not follow anywhere else. */
|
|
266
|
-
if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit'
|
|
269
|
+
if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit'))
|
|
267
270
|
this.closeFastening();
|
|
268
271
|
/*
|
|
269
272
|
* The section editor is of one part: when the selection moves off it by any way -- another part picked, the scene's
|
|
@@ -296,15 +299,10 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
296
299
|
useGizmo(mode) {
|
|
297
300
|
/*
|
|
298
301
|
R does what the bar's resize tool does: on a part whose size is not its own to drag the tool is off, so no gizmo is
|
|
299
|
-
put on it to drag in vain (:3300, 10.1.196) -- the tool's own words are said instead
|
|
300
|
-
its size fields.
|
|
302
|
+
put on it to drag in vain (:3300, 10.1.196) -- the tool's own words are said instead.
|
|
301
303
|
*/
|
|
302
304
|
if (mode === 'scale' && this.selected && this.gizmoMode !== 'scale' && !this.reachOf(this.selected).scale) {
|
|
303
|
-
|
|
304
|
-
if (asset && this.sizeDragTurned(asset, this.selected))
|
|
305
|
-
this.tellSizeByFields(this.selected);
|
|
306
|
-
else
|
|
307
|
-
this.refusals.say(i18next.t('figure.text.gizmo-not-own'));
|
|
305
|
+
this.refusals.say(i18next.t('figure.text.gizmo-not-own'));
|
|
308
306
|
return;
|
|
309
307
|
}
|
|
310
308
|
this.renderRoot.querySelector('figure-canvas')?.gizmo(mode);
|
|
@@ -333,6 +331,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
333
331
|
finally {
|
|
334
332
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
335
333
|
this.live = NOTHING_LIVE;
|
|
334
|
+
this.sizing = undefined;
|
|
336
335
|
}
|
|
337
336
|
}
|
|
338
337
|
/** The bodies drawn with the value in hand, or null when nothing is in hand (or the figure cannot take it). */
|
|
@@ -342,7 +341,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
342
341
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
343
342
|
drawnParts(geometry, live) {
|
|
344
343
|
const parts = live ?? drawableFigureParts(geometry ?? []);
|
|
345
|
-
|
|
344
|
+
const frame = this.sizing?.frame;
|
|
345
|
+
return (frame ? partsAtFrame(parts, frame.moved) : parts).filter(body => !this.hiddenParts.has(ownerOf(body.id)));
|
|
346
346
|
}
|
|
347
347
|
/**
|
|
348
348
|
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
@@ -560,64 +560,45 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
560
560
|
return none;
|
|
561
561
|
try {
|
|
562
562
|
const asset = this.shown().asset;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
A size drag shows the face it keeps and the commit keeps the same (figureScaleAnchorOf). Where figure-model cannot
|
|
566
|
-
say which face that is -- a part fastened turned or twisted -- the drag would show one result and write another,
|
|
567
|
-
so sizing by the gizmo is not offered there; the fields still size it (designer 2026-09-27).
|
|
568
|
-
*/
|
|
569
|
-
if (reach.scale && this.sizeDragUnsure(asset, id))
|
|
570
|
-
return { ...reach, scale: false };
|
|
571
|
-
return reach;
|
|
563
|
+
/* Turned any way, fastened or not: a size drag is drawn from figure-model's one size function, frame by frame (ADR-0140 4.4). */
|
|
564
|
+
return gizmoReachOf(asset, id);
|
|
572
565
|
}
|
|
573
566
|
catch {
|
|
574
567
|
return none;
|
|
575
568
|
}
|
|
576
569
|
}
|
|
577
570
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* 2026-10-19, the fan's blade-2). Its tool says it is placed by something else, as before.
|
|
571
|
+
* A size drag in hand, a frame of it: the sizes the handle comes to, through figure-model's size function and the
|
|
572
|
+
* evaluator stepped to them, drawn and written nowhere (ADR-0140 4.4). Begun at the first frame, from the figure as it
|
|
573
|
+
* is drawn and the handle the scene says was held; the release writes the last frame's changes.
|
|
582
574
|
*/
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return false;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
/** Whether a size drag on this part could not show what is written: figure-model gives no face it keeps. */
|
|
592
|
-
sizeDragUnsure(asset, id) {
|
|
575
|
+
resizing(id, result) {
|
|
576
|
+
const part = ownerOf(id);
|
|
577
|
+
const shown = this.shown();
|
|
578
|
+
if (!shown || !result.scale)
|
|
579
|
+
return;
|
|
593
580
|
try {
|
|
594
|
-
|
|
581
|
+
const drawn = drawableFigureParts(this.session.read().geometry ?? []);
|
|
582
|
+
if (this.sizing?.part !== part)
|
|
583
|
+
this.sizing = { part, drag: startSizeDrag(shown.asset, part, shown.inputs, sizeKeepOf(result.grab)), through: throughPlaneOf(shown.asset, drawn, part) };
|
|
584
|
+
const sizes = gizmoChanges(id, this.reachOf(id), { scale: result.scale }, { ...shown.inputs });
|
|
585
|
+
this.sizing = { ...this.sizing, frame: this.sizing.drag.frame(sizes) };
|
|
595
586
|
}
|
|
596
587
|
catch {
|
|
597
|
-
|
|
588
|
+
/* A size the figure cannot take on the way: the figure as it stands is drawn, and the release says why. */
|
|
589
|
+
this.sizing = undefined;
|
|
598
590
|
}
|
|
599
591
|
}
|
|
600
592
|
/**
|
|
601
|
-
*
|
|
602
|
-
*
|
|
593
|
+
* A free part a size change takes past the mounting plane: said, with 「…에 붙이기」 to fasten it there (ADR-0140 5). The
|
|
594
|
+
* size rule is not changed for it: what the author picks is the fastening, one step of the history.
|
|
603
595
|
*/
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
this.refusals.point(this.sizeDragRefusalOf(id), { label: i18next.t('figure.button.to-size-fields'), run: () => this.toSizeFields(id) });
|
|
611
|
-
}
|
|
612
|
-
/** To the part's first size field it takes typed, on the properties tab, ready to type over. */
|
|
613
|
-
async toSizeFields(id) {
|
|
614
|
-
this.middle = 'edit';
|
|
615
|
-
this.tab = 'properties';
|
|
616
|
-
this.selected = id;
|
|
617
|
-
await this.updateComplete;
|
|
618
|
-
const field = this.renderRoot.querySelector('[slot="size"] input:not([disabled]):not([readonly])');
|
|
619
|
-
field?.focus();
|
|
620
|
-
field?.select();
|
|
596
|
+
tellThroughPlane(part, mm) {
|
|
597
|
+
const plane = i18next.t('figure.label.resize-place-' + (this.asset()?.placement ?? 'floor'));
|
|
598
|
+
this.refusals.point(i18next.t('figure.text.size-through-plane', { part: partLabelOf(part), plane, mm: String(mm) }), {
|
|
599
|
+
label: i18next.t('figure.button.fasten-to-plane', { plane }),
|
|
600
|
+
run: () => this.run(() => this.session.command({ kind: 'attach', part, to: { plane: 'mounting-plane' } }))
|
|
601
|
+
});
|
|
621
602
|
}
|
|
622
603
|
/** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
|
|
623
604
|
transformed(id, result) {
|
|
@@ -645,21 +626,22 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
645
626
|
the shift had been taken for a move of the joint, and the size was dropped).
|
|
646
627
|
*/
|
|
647
628
|
const jointed = !!asset.joints?.some(j => j.body1.part === ownerOf(id));
|
|
648
|
-
/* A size drag this part cannot show truly is not written: it springs back, and the reason is said. */
|
|
649
|
-
if (result.scale && result.scale.some(k => Math.abs(k - 1) > 1e-4) && this.sizeDragTurned(asset, id)) {
|
|
650
|
-
this.tellSizeByFields(id);
|
|
651
|
-
return this.requestUpdate();
|
|
652
|
-
}
|
|
653
629
|
/*
|
|
654
|
-
A size dragged
|
|
655
|
-
|
|
630
|
+
A size dragged is written as its last frame was drawn: figure-model's one size function, keeping the side opposite
|
|
631
|
+
the handle held -- or, fastened, its fastening (ADR-0140 4.1 · 4.4). What the scene put the part at is not read.
|
|
656
632
|
*/
|
|
657
633
|
const scaled = !!result.scale && !result.rotation && result.scale.some(k => Math.abs(k - 1) > 1e-4);
|
|
658
634
|
const taken = jointed || scaled ? { scale: result.scale } : result;
|
|
659
635
|
const written = gizmoChanges(id, this.reachOf(id), taken, sizes);
|
|
660
|
-
const
|
|
636
|
+
const drawnLast = this.sizing?.part === ownerOf(id) ? this.sizing.frame : undefined;
|
|
637
|
+
const changes = !scaled ? written : drawnLast ? drawnLast.changes : figureSizeEditOf(asset, ownerOf(id), written, sizeKeepOf(result.grab)).changes;
|
|
661
638
|
if (changes.length) {
|
|
639
|
+
const through = scaled ? (this.sizing?.part === ownerOf(id) ? this.sizing.through : undefined) ?? throughPlaneOf(asset, drawableFigureParts(view.geometry), ownerOf(id)) : 0;
|
|
662
640
|
this.run(() => this.session.apply(changes));
|
|
641
|
+
/* Taken past the mounting plane by the size it was given: said with how far, once it is written, and the way to fasten it (ADR-0140 5). */
|
|
642
|
+
const now = scaled ? throughPlaneOf(this.shown().asset, drawableFigureParts(this.session.read().geometry ?? []), ownerOf(id)) : 0;
|
|
643
|
+
if (now > through + 0.5)
|
|
644
|
+
this.tellThroughPlane(ownerOf(id), now);
|
|
663
645
|
return;
|
|
664
646
|
}
|
|
665
647
|
if (jointed && result.scale) {
|
|
@@ -859,6 +841,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
859
841
|
${groups.singles.length
|
|
860
842
|
? html `<div form>${groups.singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>`
|
|
861
843
|
: nothing}
|
|
844
|
+
${this.renderSizeKeeps()}
|
|
862
845
|
</div>`
|
|
863
846
|
: nothing}
|
|
864
847
|
${place.length && !placeLocked
|
|
@@ -942,26 +925,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
942
925
|
return html `${panel('view', 'figure.label.tab-environment', html `<figure-settings .view=${this.view} @update-view=${(e) => this.setView(e.detail.view)}></figure-settings>`)}
|
|
943
926
|
${panel('history', 'figure.button.release', html `<slot name="history"></slot>`)}`;
|
|
944
927
|
}
|
|
945
|
-
/**
|
|
946
|
-
* The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
|
|
947
|
-
* while dragging and the commit keeps it, so nothing jumps on release. A part whose fastening is turned has none the
|
|
948
|
-
* rule can say, and the gizmo keeps its own (the face across from the handle).
|
|
949
|
-
*/
|
|
950
|
-
anchorsOf(asset) {
|
|
951
|
-
let next = {};
|
|
952
|
-
try {
|
|
953
|
-
const anchor = asset && this.selected ? figureScaleAnchorOf(asset, this.selected) : undefined;
|
|
954
|
-
if (anchor)
|
|
955
|
-
next = { [this.selected]: anchor };
|
|
956
|
-
}
|
|
957
|
-
catch {
|
|
958
|
-
next = {};
|
|
959
|
-
}
|
|
960
|
-
/* The same object while it says the same thing: the canvas stands its parts up again when this changes. */
|
|
961
|
-
if (JSON.stringify(next) !== JSON.stringify(this.lastAnchors))
|
|
962
|
-
this.lastAnchors = next;
|
|
963
|
-
return this.lastAnchors;
|
|
964
|
-
}
|
|
965
928
|
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
|
|
966
929
|
renderControls(view, unavailable) {
|
|
967
930
|
const asset = this.shown()?.asset;
|
|
@@ -1049,10 +1012,16 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1049
1012
|
}
|
|
1050
1013
|
changeField(field, event) {
|
|
1051
1014
|
const input = event.target, value = input.valueAsNumber;
|
|
1052
|
-
/* A
|
|
1015
|
+
/* A number typed keeps the part's middle, or its fastening -- figure-model's one size function (ADR-0140 4.1). */
|
|
1053
1016
|
const part = this.selected;
|
|
1054
1017
|
const change = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value };
|
|
1055
|
-
|
|
1018
|
+
const asset = this.shown()?.asset;
|
|
1019
|
+
const before = asset && part ? throughPlaneOf(asset, drawableFigureParts(this.session.read().geometry ?? []), part) : 0;
|
|
1020
|
+
this.run(() => this.session.apply(field.role !== 'state' && part ? figureSizeEditOf(this.shown().asset, part, [change], { keep: 'centre' }).changes : [change]));
|
|
1021
|
+
/* Typed past the mounting plane: said, with the way to fasten it there (ADR-0140 5). */
|
|
1022
|
+
const after = asset && part && field.role !== 'state' ? throughPlaneOf(this.shown().asset, drawableFigureParts(this.session.read().geometry ?? []), part) : 0;
|
|
1023
|
+
if (after > before + 0.5)
|
|
1024
|
+
this.tellThroughPlane(part, after);
|
|
1056
1025
|
// Rejected edits must also restore the displayed numeric value.
|
|
1057
1026
|
input.value = String(this.session.read().fields.find(i => i.id === field.id).value);
|
|
1058
1027
|
}
|
|
@@ -1060,7 +1029,25 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1060
1029
|
typing(field, event) {
|
|
1061
1030
|
const value = event.target.valueAsNumber;
|
|
1062
1031
|
if (Number.isFinite(value))
|
|
1063
|
-
this.hold(
|
|
1032
|
+
this.hold(this.heldOf(field, value));
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* The values in hand for a number typed or scrubbed: what its release writes -- a size with the part's middle kept,
|
|
1036
|
+
* or its fastening (figure-model's one size function, ADR-0140 4.1), so the figure does not jump on the release.
|
|
1037
|
+
*/
|
|
1038
|
+
heldOf(field, value, sized) {
|
|
1039
|
+
const part = this.selected;
|
|
1040
|
+
const asset = this.shown()?.asset;
|
|
1041
|
+
if (field.role === 'state' || !part || !asset)
|
|
1042
|
+
return { [field.id]: value };
|
|
1043
|
+
const change = { kind: 'set-design', input: field.id, unit: field.unit ?? 'mm', value };
|
|
1044
|
+
try {
|
|
1045
|
+
const { changes } = sized ? sized([change], { keep: 'centre' }) : figureSizeEditOf(asset, part, [change], { keep: 'centre' });
|
|
1046
|
+
return Object.fromEntries(changes.map(c => [c.input, c.value]));
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
return { [field.id]: value };
|
|
1050
|
+
}
|
|
1064
1051
|
}
|
|
1065
1052
|
/**
|
|
1066
1053
|
* Drag a field's label (or an axis letter) sideways to scrub its value, as vector tools do: the figure follows the
|
|
@@ -1077,10 +1064,18 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1077
1064
|
label.setPointerCapture(down.pointerId);
|
|
1078
1065
|
const start = field.value;
|
|
1079
1066
|
let value = start;
|
|
1067
|
+
/* Prepared once for the scrub: each move is a size through the same function its release writes with. */
|
|
1068
|
+
const asset = this.shown()?.asset;
|
|
1069
|
+
const sized = asset && this.selected && field.role !== 'state' ? (() => { try {
|
|
1070
|
+
return figureSizeEditorOf(asset, this.selected);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
} })() : undefined;
|
|
1080
1075
|
const move = (e) => {
|
|
1081
1076
|
value = scrubbed(start, e.clientX - down.clientX, scrubStep(field.unit, start, e.shiftKey));
|
|
1082
1077
|
input.value = String(value);
|
|
1083
|
-
this.hold(
|
|
1078
|
+
this.hold(this.heldOf(field, value, sized));
|
|
1084
1079
|
};
|
|
1085
1080
|
const up = (e) => {
|
|
1086
1081
|
label.removeEventListener('pointermove', move);
|
|
@@ -1125,26 +1120,33 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1125
1120
|
return { of, toDeclare };
|
|
1126
1121
|
});
|
|
1127
1122
|
const range = measured.of;
|
|
1128
|
-
/*
|
|
1123
|
+
/*
|
|
1124
|
+
In words a person reads (user 2026-10-19/20): what the figure does on a drawing, not the inputs behind it. Fixed,
|
|
1125
|
+
it says the size it is always drawn at; resizable, the sizes it may take on each axis and the parts that change
|
|
1126
|
+
with it. The way to it stays one press -- a figure no part follows (a robot scaled whole) reaches the one-ratio
|
|
1127
|
+
rule only from here -- and the help mark says how parts are made to follow.
|
|
1128
|
+
*/
|
|
1129
|
+
const mm = (v) => Math.round(v).toLocaleString();
|
|
1129
1130
|
const toDeclare = measured.toDeclare;
|
|
1131
|
+
const title = (explained) => html `<h3>${i18next.t('figure.label.board-size')}${this.help.button(explained)}</h3>${this.help.hint(explained)}`;
|
|
1130
1132
|
if (!range && toDeclare)
|
|
1131
|
-
return html `<section>
|
|
1132
|
-
|
|
1133
|
-
<p
|
|
1133
|
+
return html `<section board-size>
|
|
1134
|
+
${title('figure.text.board-size-fixed-explained')}
|
|
1135
|
+
<p fixed-size>${i18next.t('figure.text.board-size-fixed', { x: mm(toDeclare.at.x), y: mm(toDeclare.at.y), z: mm(toDeclare.at.z) })}</p>
|
|
1134
1136
|
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })}>
|
|
1135
|
-
<md-icon>
|
|
1137
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
|
|
1136
1138
|
</button>
|
|
1137
1139
|
</section>`;
|
|
1138
1140
|
if (!range)
|
|
1139
1141
|
return nothing;
|
|
1140
|
-
const mm = (v) => Math.round(v).toLocaleString();
|
|
1141
1142
|
const differs = rangeDiffers(range.declared, range.measured);
|
|
1142
|
-
|
|
1143
|
-
|
|
1143
|
+
const followers = sizeFollowersOf(asset);
|
|
1144
|
+
return html `<section board-size>
|
|
1145
|
+
${title('figure.text.board-size-range-explained')}
|
|
1144
1146
|
${['x', 'y', 'z'].map(axis => html `<div inline-field>
|
|
1145
|
-
<label>${
|
|
1146
|
-
<div pair>
|
|
1147
|
-
${['min', 'max'].map(bound => html `<input
|
|
1147
|
+
<label>${i18next.t('figure.label.resize-' + axis)} <span unit>mm</span></label>
|
|
1148
|
+
<div pair range>
|
|
1149
|
+
${['min', 'max'].map((bound, i) => html `${i ? html `<span dash aria-hidden="true">–</span>` : nothing}<input
|
|
1148
1150
|
aria-label=${`size.${axis}.${bound}`}
|
|
1149
1151
|
type="number"
|
|
1150
1152
|
step="any"
|
|
@@ -1158,6 +1160,13 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1158
1160
|
/>`)}
|
|
1159
1161
|
</div>
|
|
1160
1162
|
</div>`)}
|
|
1163
|
+
<p followers>
|
|
1164
|
+
${followers.length
|
|
1165
|
+
? i18next.t('figure.text.board-size-followers', {
|
|
1166
|
+
parts: followers.length > 4 ? i18next.t('figure.text.board-size-followers-more', { first: followers.slice(0, 4).join(', '), rest: String(followers.length - 4) }) : followers.join(', ')
|
|
1167
|
+
})
|
|
1168
|
+
: i18next.t('figure.text.board-size-no-followers')}
|
|
1169
|
+
</p>
|
|
1161
1170
|
${differs
|
|
1162
1171
|
? html `<p hint>
|
|
1163
1172
|
${i18next.t('figure.text.instance-range-can-widen', {
|
|
@@ -1504,46 +1513,51 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1504
1513
|
this.openFastening();
|
|
1505
1514
|
}
|
|
1506
1515
|
openFastening() {
|
|
1507
|
-
if (!this.selected || this.
|
|
1516
|
+
if (!this.selected || this.fastenPopup)
|
|
1508
1517
|
return;
|
|
1509
1518
|
this.fastenError = '';
|
|
1510
1519
|
this.fastenHover = null;
|
|
1511
|
-
/* One panel over the side at a time: the view or the history gives way to the window. */
|
|
1512
|
-
this.overlay = undefined;
|
|
1513
1520
|
this.fastening = true;
|
|
1521
|
+
const content = document.createElement('figure-fasten-dialog');
|
|
1522
|
+
content.addEventListener('preview-command', e => this.commandPreview.ask(e.detail));
|
|
1523
|
+
content.addEventListener('fasten-hover', e => this.hoverFastening(e.detail));
|
|
1524
|
+
content.addEventListener('fasten-apply', e => this.applyFastening(e.detail.action));
|
|
1525
|
+
content.addEventListener('fasten-cancel', () => this.closeFastening());
|
|
1526
|
+
this.fastenWindow = content;
|
|
1527
|
+
this.tellFastening();
|
|
1528
|
+
/* No backdrop: the canvas stays reachable while it is open -- a part pressed there is aimed at (ADR-0140 stage 4). */
|
|
1529
|
+
const popup = openPopup(html `${content}`, { title: i18next.t('figure.label.fastening'), size: 'fit', backdrop: false, movable: true, closable: true, escapable: true });
|
|
1530
|
+
this.fastenPopup = popup;
|
|
1531
|
+
/* Closed by its own X or Esc as well: the editor lets go of it the same way. */
|
|
1532
|
+
popup.result.then(() => this.fastenPopup === popup && this.fastenClosed());
|
|
1514
1533
|
}
|
|
1515
1534
|
closeFastening() {
|
|
1535
|
+
const popup = this.fastenPopup;
|
|
1536
|
+
this.fastenClosed();
|
|
1537
|
+
popup?.close();
|
|
1538
|
+
}
|
|
1539
|
+
fastenClosed() {
|
|
1540
|
+
this.fastenPopup = undefined;
|
|
1541
|
+
this.fastenWindow = undefined;
|
|
1516
1542
|
this.fastening = false;
|
|
1517
1543
|
this.fastenError = '';
|
|
1518
1544
|
this.fastenHover = null;
|
|
1519
1545
|
}
|
|
1520
|
-
/**
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
overlay-body
|
|
1536
|
-
.revision=${this.shown()}
|
|
1537
|
-
.partId=${this.selected}
|
|
1538
|
-
?locked=${unavailable}
|
|
1539
|
-
.previewWords=${this.commandPreview.words()}
|
|
1540
|
-
.error=${this.fastenError}
|
|
1541
|
-
@preview-command=${(e) => this.commandPreview.ask(e.detail)}
|
|
1542
|
-
@fasten-hover=${(e) => this.hoverFastening(e.detail)}
|
|
1543
|
-
@fasten-apply=${(e) => this.applyFastening(e.detail.action)}
|
|
1544
|
-
@fasten-cancel=${() => this.closeFastening()}
|
|
1545
|
-
></figure-fasten-dialog>
|
|
1546
|
-
</div>`;
|
|
1546
|
+
/** What the window shows is the editor's: the figure on screen, the part, and what the preview and a refusal said. */
|
|
1547
|
+
tellFastening() {
|
|
1548
|
+
const content = this.fastenWindow;
|
|
1549
|
+
const view = this.session?.read();
|
|
1550
|
+
if (!content || !view)
|
|
1551
|
+
return;
|
|
1552
|
+
content.revision = this.shown();
|
|
1553
|
+
content.partId = this.selected;
|
|
1554
|
+
content.locked = view.loading || !!view.error || view.saving || !view.context;
|
|
1555
|
+
content.previewWords = this.commandPreview.words();
|
|
1556
|
+
content.error = this.fastenError;
|
|
1557
|
+
}
|
|
1558
|
+
disconnectedCallback() {
|
|
1559
|
+
super.disconnectedCallback();
|
|
1560
|
+
this.closeFastening();
|
|
1547
1561
|
}
|
|
1548
1562
|
/** 「적용」: the command, one step; refused, the window stays open and says why. */
|
|
1549
1563
|
applyFastening(action) {
|
|
@@ -1606,15 +1620,16 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1606
1620
|
return this.shown()?.asset;
|
|
1607
1621
|
}
|
|
1608
1622
|
/** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
|
|
1609
|
-
renderTab(tab, icon, label
|
|
1623
|
+
renderTab(tab, icon, label) {
|
|
1610
1624
|
/* Its accessible name is the name it shows (designer 2026-09-27); the longer title stays as its tooltip. */
|
|
1611
1625
|
const name = i18next.t('figure.label.tab-short-' + tab);
|
|
1612
1626
|
return html `<button tab ?on=${this.tab === tab} title=${label} aria-label=${name} @click=${() => (this.tab = tab)}>
|
|
1613
|
-
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span
|
|
1627
|
+
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span>
|
|
1614
1628
|
</button>`;
|
|
1615
1629
|
}
|
|
1616
1630
|
updated(changed) {
|
|
1617
1631
|
super.updated?.(changed);
|
|
1632
|
+
this.tellFastening();
|
|
1618
1633
|
/* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
|
|
1619
1634
|
if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen)
|
|
1620
1635
|
this.blockersOpen = false;
|
|
@@ -1641,7 +1656,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1641
1656
|
return nothing;
|
|
1642
1657
|
const asset = this.asset();
|
|
1643
1658
|
const measures = figureSectionMeasuresOf(asset, this.selected);
|
|
1644
|
-
|
|
1659
|
+
/* What a measure typed here keeps: the part's middle, or its fastening (ADR-0140 4.1). */
|
|
1660
|
+
const kept = keptBySizeOf(asset, this.selected);
|
|
1645
1661
|
return html `<figure-section-editor
|
|
1646
1662
|
.body=${{ ...part, id: this.selected }}
|
|
1647
1663
|
.measures=${measures}
|
|
@@ -1793,13 +1809,12 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1793
1809
|
held = undefined;
|
|
1794
1810
|
}
|
|
1795
1811
|
const roomFields = own.filter(i => roomOnly.includes(i.id));
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
: 'figure.text.saved';
|
|
1812
|
+
/*
|
|
1813
|
+
Only what is under way is said. Saved or not is the save button's: filled and pressable when there is something
|
|
1814
|
+
to save, dimmed when not -- the saved and unsaved words beside it said the same thing again in the primary
|
|
1815
|
+
colour, read as a button (screen copy rule: a state is the control's).
|
|
1816
|
+
*/
|
|
1817
|
+
const status = view.loading ? 'figure.text.loading-figure' : view.saving ? 'figure.text.saving-figure' : '';
|
|
1803
1818
|
/*
|
|
1804
1819
|
A figure with no parts yet is not one that failed: what blocks its release is said once it has something to
|
|
1805
1820
|
release (first 15 minutes, 2026-10-16 evening -- red from the first click read as a failure). The verdict tab
|
|
@@ -1825,7 +1840,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1825
1840
|
${['edit', 'preview'].map(mode => html `<button slot="mode" mode aria-pressed=${String(this.middle === mode)} title=${i18next.t('figure.text.mode-' + mode + '-explained')} @click=${() => (this.middle = mode)}>
|
|
1826
1841
|
${i18next.t('figure.label.mode-' + mode)}
|
|
1827
1842
|
</button>`)}
|
|
1828
|
-
|
|
1843
|
+
${status ? html `<span slot="status" class="status" role="status">${i18next.t(status)}</span>` : nothing}
|
|
1829
1844
|
<span slot="status" release-state>${this.releaseWords(view)}</span>
|
|
1830
1845
|
${blockers.length
|
|
1831
1846
|
? html `<button slot="commit" release-blocked aria-expanded=${String(this.blockersOpen)} @click=${() => (this.blockersOpen = !this.blockersOpen)}>
|
|
@@ -1907,7 +1922,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1907
1922
|
<figure-canvas
|
|
1908
1923
|
.view=${this.view}
|
|
1909
1924
|
.parts=${this.drawnParts(view.geometry, live)}
|
|
1910
|
-
.anchors=${this.anchorsOf(asset)}
|
|
1911
1925
|
.fan=${this.fanOf(shown, live)}
|
|
1912
1926
|
.ghost=${this.previewShown().ghost}
|
|
1913
1927
|
.highlight=${this.previewShown().highlight}
|
|
@@ -1915,9 +1929,12 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1915
1929
|
.palette=${view.palette ?? {}}
|
|
1916
1930
|
.context=${view.context ?? ''}
|
|
1917
1931
|
.selected=${this.selected}
|
|
1932
|
+
?aiming=${this.fastening}
|
|
1918
1933
|
.reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
|
|
1934
|
+
@part-sizing=${(e) => this.resizing(e.detail.id, e.detail.result)}
|
|
1919
1935
|
@part-transformed=${(e) => this.transformed(e.detail.id, e.detail.result)}
|
|
1920
1936
|
@part-selected=${(e) => this.choose(e.detail)}
|
|
1937
|
+
@part-aimed=${(e) => this.fastenWindow?.aimAt?.(e.detail)}
|
|
1921
1938
|
@render-error=${(e) => {
|
|
1922
1939
|
this.error = e.detail.message;
|
|
1923
1940
|
}}
|
|
@@ -1929,7 +1946,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1929
1946
|
${this.renderTab('motion', 'animation', i18next.t('figure.label.motion'))}
|
|
1930
1947
|
${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
|
|
1931
1948
|
${this.renderTab('data', 'sensors', i18next.t('figure.label.tab-data'))}
|
|
1932
|
-
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity')
|
|
1949
|
+
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'))}
|
|
1933
1950
|
</div>
|
|
1934
1951
|
<slot name="notice"></slot>
|
|
1935
1952
|
<div panel ?off=${tab !== 'properties'}>
|
|
@@ -2005,7 +2022,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
2005
2022
|
<div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
|
|
2006
2023
|
</aside>
|
|
2007
2024
|
</ox-three-pane>
|
|
2008
|
-
${this.renderOverlay()}
|
|
2025
|
+
${this.renderOverlay()}`;
|
|
2009
2026
|
}
|
|
2010
2027
|
/** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
|
|
2011
2028
|
renderGroups(groups, vectorTitle, singleTitle, unavailable, tools = null, held) {
|
|
@@ -2076,6 +2093,12 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
2076
2093
|
* rule" named nothing): a fastened part's place and turn are its fastening's; other numbers are worked out from
|
|
2077
2094
|
* something else. A size that follows something is said at the panel's head, so not again here.
|
|
2078
2095
|
*/
|
|
2096
|
+
/** What a size typed keeps, under the size fields: the part's middle, where it is fastened, or only its size (ADR-0140 4.1). */
|
|
2097
|
+
renderSizeKeeps() {
|
|
2098
|
+
const asset = this.asset();
|
|
2099
|
+
const kept = asset && this.selected ? keptBySizeOf(asset, this.selected) : undefined;
|
|
2100
|
+
return kept ? html `<p hint size-keeps>${i18next.t(SIZE_KEEPS_WORDS[kept])}</p>` : nothing;
|
|
2101
|
+
}
|
|
2079
2102
|
renderHeldAxes(vectors) {
|
|
2080
2103
|
const fastened = !!(this.asset())?.joints?.some(j => j.body1.part === ownerOf(this.selected));
|
|
2081
2104
|
const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && ['x', 'y', 'z'].some(axis => !g.cells[axis]));
|
|
@@ -2196,10 +2219,6 @@ __decorate([
|
|
|
2196
2219
|
property({ type: Number }),
|
|
2197
2220
|
__metadata("design:type", Object)
|
|
2198
2221
|
], FigureEditor.prototype, "revision", void 0);
|
|
2199
|
-
__decorate([
|
|
2200
|
-
property({ type: Number }),
|
|
2201
|
-
__metadata("design:type", Number)
|
|
2202
|
-
], FigureEditor.prototype, "instances", void 0);
|
|
2203
2222
|
__decorate([
|
|
2204
2223
|
property({ attribute: false }),
|
|
2205
2224
|
__metadata("design:type", Object)
|
|
@@ -2284,6 +2303,10 @@ __decorate([
|
|
|
2284
2303
|
state(),
|
|
2285
2304
|
__metadata("design:type", Object)
|
|
2286
2305
|
], FigureEditor.prototype, "live", void 0);
|
|
2306
|
+
__decorate([
|
|
2307
|
+
state(),
|
|
2308
|
+
__metadata("design:type", Object)
|
|
2309
|
+
], FigureEditor.prototype, "sizing", void 0);
|
|
2287
2310
|
__decorate([
|
|
2288
2311
|
state(),
|
|
2289
2312
|
__metadata("design:type", Object)
|