@things-factory/figure-ui 10.1.208 → 10.1.211
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/data-interface.ts +5 -5
- package/client/modeller/editor.ts +97 -88
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +87 -157
- package/client/modeller/figure-size-editor.ts +233 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +87 -36
- package/client/modeller/live.ts +33 -0
- package/client/modeller/motion-ends.ts +10 -6
- package/client/modeller/size-response.ts +95 -0
- package/dist-client/modeller/data-interface.d.ts +1 -1
- package/dist-client/modeller/data-interface.js +5 -5
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +14 -1
- package/dist-client/modeller/editor.js +103 -89
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.js +1 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -4
- package/dist-client/modeller/figure-links.js +91 -145
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +29 -0
- package/dist-client/modeller/figure-size-editor.js +262 -0
- package/dist-client/modeller/figure-size-editor.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +1 -1
- package/dist-client/modeller/joint-drag.js +27 -22
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/joint-fan.js +4 -3
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +21 -9
- package/dist-client/modeller/links.js +85 -35
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +14 -0
- package/dist-client/modeller/live.js +31 -0
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +4 -1
- package/dist-client/modeller/motion-ends.js +10 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +29 -0
- package/dist-client/modeller/size-response.js +99 -0
- package/dist-client/modeller/size-response.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
- package/docs/evidence/2026-10-24/integrated-size.png +0 -0
- package/docs/evidence/2026-10-24/multipart-response.png +0 -0
- package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
- package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
- package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/width-preview.png +0 -0
- package/docs/multipart-response-review-2026-10-24.md +77 -0
- package/docs/response-authoring-2026-10-24.md +46 -0
- package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
- package/package.json +7 -7
- package/test/attachment-size-contract.test.ts +47 -0
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +66 -14
- package/test/browser/links.check.mjs +41 -22
- package/test/browser/modeller-page.js +1 -1
- package/test/browser/modeller-revision.check.mjs +21 -7
- package/test/browser/modeller.check.mjs +106 -39
- package/test/browser/multipart-response-page.js +8 -0
- package/test/browser/parts.check.mjs +2 -1
- package/test/browser/product-page.js +1 -1
- package/test/browser/props.check.mjs +17 -14
- package/test/browser/response-page.js +12 -0
- package/test/browser/row-preview.check.mjs +3 -3
- package/test/browser/screen.check.mjs +57 -13
- package/test/browser/serve-response.mjs +24 -0
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +2 -2
- package/test/command-preview.test.ts +7 -7
- package/test/joint-drag.test.ts +6 -6
- package/test/joint-frames.test.ts +33 -32
- package/test/joints.ts +1 -6
- package/test/links.test.ts +60 -20
- package/test/multipart-response-fixture.js +17 -0
- package/test/multipart-response.test.ts +37 -0
- package/test/refusals.test.ts +2 -2
- package/test/resize-with-placement.test.ts +23 -1
- package/test/size-response.test.ts +49 -0
- package/test/size-source-pairing.test.ts +66 -0
- package/translations/en.json +47 -10
- package/translations/es.json +47 -10
- package/translations/ja.json +47 -10
- package/translations/ko.json +52 -15
- package/translations/ms.json +47 -10
- package/translations/zh.json +47 -10
|
@@ -6,7 +6,7 @@ import '@operato/layout';
|
|
|
6
6
|
import { openPopup } from '@operato/layout';
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
8
8
|
import { live } from 'lit/directives/live.js';
|
|
9
|
-
import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf } from '@hatiolab/figure-model';
|
|
9
|
+
import { figureFastenAxesOf, figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf } from '@hatiolab/figure-model';
|
|
10
10
|
import './figure-recipe-catalog.js';
|
|
11
11
|
import { captureFigureThumbnail } from './thumbnail.js';
|
|
12
12
|
import { drawableFigureParts } from './renderer.js';
|
|
@@ -50,13 +50,14 @@ import { nudgeOf, placementOf } from './stage-model.js';
|
|
|
50
50
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js';
|
|
51
51
|
import { editRefusalOf, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js';
|
|
52
52
|
import { HelpToggles } from './help.js';
|
|
53
|
-
import {
|
|
53
|
+
import { sizeInputOf } from './size-response.js';
|
|
54
|
+
import { canFollow, followableAxes, endPlaceKeyOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js';
|
|
54
55
|
import { jointFanOf } from './joint-fan.js';
|
|
55
56
|
import { derivedOf } from './revision.js';
|
|
56
57
|
import { jointDragOf } from './joint-drag.js';
|
|
57
|
-
import { liveOverSession, NOTHING_LIVE,
|
|
58
|
+
import { liveOverSession, NOTHING_LIVE, scrubLabel } from './live.js';
|
|
58
59
|
import { startingShapeKindOfBody } from './starting-shape.js';
|
|
59
|
-
import {
|
|
60
|
+
import { GroupMenuStyles } from './group-menu.js';
|
|
60
61
|
/** The person's own fold of the properties' 「더 보기」, kept in their browser. */
|
|
61
62
|
const MORE_OPEN_KEY = 'figure-properties-more-open';
|
|
62
63
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
@@ -330,10 +331,16 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
330
331
|
}
|
|
331
332
|
/**
|
|
332
333
|
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
333
|
-
* (the network, the server) as it came.
|
|
334
|
+
* (the network, the server) as it came. A loop is said as a loop whichever way it was made: a command, or the size
|
|
335
|
+
* editor's steps (a measure made to follow what follows it, 2026-10-24 -- said as a bare refused step before).
|
|
334
336
|
*/
|
|
335
337
|
said(error) {
|
|
336
|
-
|
|
338
|
+
const code = error?.code;
|
|
339
|
+
if (code === 'DIMENSION_CYCLE')
|
|
340
|
+
return i18next.t('figure.text.follow-would-loop');
|
|
341
|
+
if (code === 'FASTEN_CYCLE' || code === 'ATTACH_CYCLE')
|
|
342
|
+
return i18next.t('figure.text.fasten-would-loop');
|
|
343
|
+
return code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error);
|
|
337
344
|
}
|
|
338
345
|
run(action) {
|
|
339
346
|
try {
|
|
@@ -824,6 +831,10 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
824
831
|
.partId=${this.selected}
|
|
825
832
|
?locked=${unavailable}
|
|
826
833
|
@command=${(e) => this.linkCommand(e.detail.action)}
|
|
834
|
+
@size-recipe=${(e) => this.applySizeSteps(e.detail.steps)}
|
|
835
|
+
@size-input=${(e) => this.writeField({ id: e.detail.input, unit: 'mm' }, e.detail.value)}
|
|
836
|
+
@size-typing=${(e) => this.hold(this.heldOf({ id: e.detail.input, unit: 'mm' }, e.detail.value))}
|
|
837
|
+
@size-let-go=${() => (this.live = NOTHING_LIVE)}
|
|
827
838
|
@fasten-open=${() => this.openFastening()}
|
|
828
839
|
.previewWords=${this.commandPreview.words()}
|
|
829
840
|
.detachWords=${this.commandPreview.detachWords(shown)}
|
|
@@ -843,18 +854,22 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
843
854
|
*/
|
|
844
855
|
renderPartGroups(shown, geometry, fields, unavailable, held) {
|
|
845
856
|
const groups = this.withSizeRow(groupFields(fields, this.selected), held);
|
|
846
|
-
const
|
|
857
|
+
const integrated = !!shown && canFollow(shown.asset, this.selected);
|
|
858
|
+
const dimensionInputs = new Set(integrated ? followableAxes(shown.asset, this.selected).map(axis => sizeInputOf(shown.asset, this.selected, axis)) : []);
|
|
859
|
+
const singles = groups.singles.filter(g => g.kind !== 'single' || !dimensionInputs.has(g.field.id));
|
|
860
|
+
const size = integrated ? [] : groups.vectors.filter(g => g.kind === 'vector' && g.name === 'size');
|
|
847
861
|
const place = groups.vectors.filter(g => g.kind === 'vector' && g.name !== 'size');
|
|
848
862
|
const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(this.selected));
|
|
849
863
|
const placeLocked = fastened && place.every(g => g.kind === 'vector' && ['x', 'y', 'z'].every(axis => !g.cells[axis]));
|
|
850
|
-
return this.renderLinks(unavailable, 'shape', html `${size.length ||
|
|
851
|
-
? html `<
|
|
864
|
+
return this.renderLinks(unavailable, 'shape', html `${size.length || singles.length
|
|
865
|
+
? html `<section slot="size" group-fields>
|
|
866
|
+
<h3>${i18next.t(integrated ? 'figure.label.group-shape' : 'figure.label.group-size')}</h3>
|
|
852
867
|
${size.map(g => this.renderVector(g, unavailable, held))}
|
|
853
|
-
${
|
|
854
|
-
? html `<div form>${
|
|
868
|
+
${singles.length
|
|
869
|
+
? html `<div form>${singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>`
|
|
855
870
|
: nothing}
|
|
856
|
-
${this.renderSizeKeeps()}
|
|
857
|
-
</
|
|
871
|
+
${!integrated ? this.renderSizeKeeps() : nothing}
|
|
872
|
+
</section>`
|
|
858
873
|
: nothing}
|
|
859
874
|
${place.length && !placeLocked
|
|
860
875
|
? html `<div slot="place" group-fields>${place.map(g => this.renderVector(g, unavailable, held))}${this.renderHeldAxes(place)}</div>`
|
|
@@ -862,7 +877,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
862
877
|
}
|
|
863
878
|
/** The 「반응형」 tab of a part: the links' response groups, its row's own section put in by slot. */
|
|
864
879
|
renderResponse(shown, geometry, unavailable) {
|
|
865
|
-
return this.renderLinks(unavailable, 'response', this.renderRepeat(shown, geometry, unavailable));
|
|
880
|
+
return this.renderLinks(unavailable, 'response', html `<div slot="size-note">${this.renderSizeKeeps()}</div>${this.renderRepeat(shown, geometry, unavailable)}`);
|
|
866
881
|
}
|
|
867
882
|
/**
|
|
868
883
|
* 「더 보기」: what few parts need, folded by default and left as this person last left it (chief architect
|
|
@@ -970,6 +985,25 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
970
985
|
if (done && held.from === 'section-editor')
|
|
971
986
|
this.closeSectionEditor();
|
|
972
987
|
}
|
|
988
|
+
/** The figure's size, declared for a part to follow, in one line (ADR ruling Q1). */
|
|
989
|
+
sizeDeclaredWords() {
|
|
990
|
+
const size = this.designValues();
|
|
991
|
+
return i18next.t('figure.text.size-declared-to-follow', {
|
|
992
|
+
x: Math.round(size['size.x'] ?? 0).toLocaleString(),
|
|
993
|
+
y: Math.round(size['size.y'] ?? 0).toLocaleString(),
|
|
994
|
+
z: Math.round(size['size.z'] ?? 0).toLocaleString()
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* The size editor's steps, one step of the history. A measure made to follow the figure on a figure with no size
|
|
999
|
+
* declares it on the way; that is said in one line, as follow-size said it (ADR ruling Q1, 2026-10-24).
|
|
1000
|
+
*/
|
|
1001
|
+
applySizeSteps(steps) {
|
|
1002
|
+
const hadSize = this.hasSize();
|
|
1003
|
+
this.run(() => this.session.applyRecipe(steps));
|
|
1004
|
+
if (!hadSize && !this.error && this.hasSize())
|
|
1005
|
+
this.notice = this.sizeDeclaredWords();
|
|
1006
|
+
}
|
|
973
1007
|
/** Runs a command; true when it was taken. */
|
|
974
1008
|
linkCommand(action, from = 'panel') {
|
|
975
1009
|
/* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
|
|
@@ -981,14 +1015,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
981
1015
|
this.error = '';
|
|
982
1016
|
this.faceMapping = undefined;
|
|
983
1017
|
/* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
|
|
984
|
-
if (action.kind === 'follow-size' && !hadSize)
|
|
985
|
-
|
|
986
|
-
this.notice = i18next.t('figure.text.size-declared-to-follow', {
|
|
987
|
-
x: Math.round(size['size.x'] ?? 0).toLocaleString(),
|
|
988
|
-
y: Math.round(size['size.y'] ?? 0).toLocaleString(),
|
|
989
|
-
z: Math.round(size['size.z'] ?? 0).toLocaleString()
|
|
990
|
-
});
|
|
991
|
-
}
|
|
1018
|
+
if (action.kind === 'follow-size' && !hadSize)
|
|
1019
|
+
this.notice = this.sizeDeclaredWords();
|
|
992
1020
|
this.changed();
|
|
993
1021
|
return true;
|
|
994
1022
|
}
|
|
@@ -1003,18 +1031,20 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1003
1031
|
this.error = '';
|
|
1004
1032
|
return false;
|
|
1005
1033
|
}
|
|
1006
|
-
this.error =
|
|
1007
|
-
code === 'DIMENSION_CYCLE'
|
|
1008
|
-
? i18next.t('figure.text.follow-would-loop')
|
|
1009
|
-
: code === 'FASTEN_CYCLE' || code === 'ATTACH_CYCLE'
|
|
1010
|
-
? i18next.t('figure.text.fasten-would-loop')
|
|
1011
|
-
: this.said(error);
|
|
1034
|
+
this.error = this.said(error);
|
|
1012
1035
|
return false;
|
|
1013
1036
|
}
|
|
1014
1037
|
}
|
|
1015
1038
|
changeField(field, event) {
|
|
1016
|
-
const input = event.target
|
|
1017
|
-
|
|
1039
|
+
const input = event.target;
|
|
1040
|
+
// Rejected edits must also restore the displayed numeric value.
|
|
1041
|
+
input.value = String(this.writeField(field, input.valueAsNumber));
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* A number written to an input, from a field, a scrub or the size editor's rows: a size keeps the part's middle, or its
|
|
1045
|
+
* fastening -- figure-model's one size function (ADR-0140 4.1). Gives the value the input holds after, refused or not.
|
|
1046
|
+
*/
|
|
1047
|
+
writeField(field, value) {
|
|
1018
1048
|
const part = this.selected;
|
|
1019
1049
|
const change = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value };
|
|
1020
1050
|
const asset = this.shown()?.asset;
|
|
@@ -1024,8 +1054,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1024
1054
|
const after = asset && part && field.role !== 'state' ? throughPlaneOf(this.shown().asset, drawableFigureParts(this.session.read().geometry ?? []), part) : 0;
|
|
1025
1055
|
if (after > before + 0.5)
|
|
1026
1056
|
this.tellThroughPlane(part, after);
|
|
1027
|
-
|
|
1028
|
-
input.value = String(this.session.read().fields.find(i => i.id === field.id).value);
|
|
1057
|
+
return this.session.read().fields.find(i => i.id === field.id).value;
|
|
1029
1058
|
}
|
|
1030
1059
|
/** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
|
|
1031
1060
|
typing(field, event) {
|
|
@@ -1056,16 +1085,12 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1056
1085
|
* pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
|
|
1057
1086
|
*/
|
|
1058
1087
|
scrub(field, down, unavailable) {
|
|
1059
|
-
if (unavailable
|
|
1088
|
+
if (unavailable)
|
|
1060
1089
|
return;
|
|
1061
1090
|
const label = down.currentTarget;
|
|
1062
1091
|
const input = (label.closest('div[cell]')?.querySelector('input') ?? label.nextElementSibling);
|
|
1063
1092
|
if (!input)
|
|
1064
1093
|
return;
|
|
1065
|
-
down.preventDefault();
|
|
1066
|
-
label.setPointerCapture(down.pointerId);
|
|
1067
|
-
const start = field.value;
|
|
1068
|
-
let value = start;
|
|
1069
1094
|
/* Prepared once for the scrub: each move is a size through the same function its release writes with. */
|
|
1070
1095
|
const asset = this.shown()?.asset;
|
|
1071
1096
|
const sized = asset && this.selected && field.role !== 'state' ? (() => { try {
|
|
@@ -1074,29 +1099,17 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1074
1099
|
catch {
|
|
1075
1100
|
return undefined;
|
|
1076
1101
|
} })() : undefined;
|
|
1077
|
-
|
|
1078
|
-
value
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
label.removeEventListener('pointercancel', up);
|
|
1086
|
-
/* A drag the system took away is not the author's value: the field goes back to where it was. */
|
|
1087
|
-
if (e.type === 'pointercancel')
|
|
1088
|
-
value = start;
|
|
1089
|
-
if (value === start) {
|
|
1090
|
-
input.value = String(start);
|
|
1102
|
+
scrubLabel(down, field, {
|
|
1103
|
+
move: value => {
|
|
1104
|
+
input.value = String(value);
|
|
1105
|
+
this.hold(this.heldOf(field, value, sized));
|
|
1106
|
+
},
|
|
1107
|
+
release: value => (input.value = String(this.writeField(field, value))),
|
|
1108
|
+
cancel: () => {
|
|
1109
|
+
input.value = String(field.value);
|
|
1091
1110
|
this.live = NOTHING_LIVE;
|
|
1092
|
-
return;
|
|
1093
1111
|
}
|
|
1094
|
-
|
|
1095
|
-
this.changeField(field, { target: input });
|
|
1096
|
-
};
|
|
1097
|
-
label.addEventListener('pointermove', move);
|
|
1098
|
-
label.addEventListener('pointerup', up);
|
|
1099
|
-
label.addEventListener('pointercancel', up);
|
|
1112
|
+
});
|
|
1100
1113
|
}
|
|
1101
1114
|
/** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
|
|
1102
1115
|
renderInstanceRange(shown, unavailable) {
|
|
@@ -1256,45 +1269,46 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1256
1269
|
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
|
|
1257
1270
|
const own = `${this.selected}.repeat.pitch`;
|
|
1258
1271
|
/* The pitch as it stands: the part's own number, a constant, or what a pitch that follows a length comes to. */
|
|
1259
|
-
const pitch = asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(shown, layout?.args[4]);
|
|
1260
1272
|
const fits = layout?.op === 'fit-pitch@1';
|
|
1261
|
-
|
|
1262
|
-
const lengthRef = layout?.args[0] ?? '';
|
|
1263
|
-
const length = asset.designInputs[lengthRef];
|
|
1264
|
-
const longest = model.inputs.find(i => i.id === lengthRef)?.max;
|
|
1265
|
-
const limit = model.constants.find(c => c.id === layout?.args[6])?.value;
|
|
1273
|
+
const pitch = fits ? asset.designInputs[own] ?? model.constants.find(c => c.id === layout?.args[4])?.value ?? valueNow(shown, layout?.args[4]) : this.rowNow(shown)?.pitch;
|
|
1266
1274
|
const field = { id: own, unit: 'mm', role: 'design', value: pitch ?? 0 };
|
|
1267
|
-
const
|
|
1275
|
+
const record = asset.joints?.find(j => j.body1.part === this.selected && j.repeat);
|
|
1276
|
+
const parent = record && 'part' in record.body0 ? record.body0.part : undefined;
|
|
1277
|
+
const removable = !!record;
|
|
1278
|
+
const axes = record && 'part' in record.body0 && 'face' in record.body0 ? figureFastenAxesOf(asset, record.body0) : [];
|
|
1279
|
+
const update = (change) => {
|
|
1280
|
+
if (!record?.repeat || unavailable)
|
|
1281
|
+
return;
|
|
1282
|
+
this.run(() => this.session.command({ kind: 'set-joint', joint: { ...record, repeat: { ...record.repeat, ...change } } }));
|
|
1283
|
+
};
|
|
1268
1284
|
return html `<section slot="row" row>
|
|
1269
1285
|
<h3>
|
|
1270
1286
|
${i18next.t('figure.label.repeat')}
|
|
1271
|
-
|
|
1287
|
+
<span section-state active>${i18next.t('figure.label.repeat-active')}</span>
|
|
1272
1288
|
</h3>
|
|
1273
|
-
<div
|
|
1274
|
-
|
|
1289
|
+
<div choice-row role="radiogroup" aria-label=${i18next.t('figure.label.row-basis')}>
|
|
1290
|
+
<label toggle><input type="radio" name="applied-repeat-mode" .checked=${live(fits)} ?disabled=${unavailable || !record} @change=${() => update({ count: undefined, pitch: pitch && pitch > 0 ? pitch : 200 })}>${i18next.t('figure.label.repeat-fills')}</label>
|
|
1291
|
+
<label toggle><input type="radio" name="applied-repeat-mode" .checked=${live(!fits)} ?disabled=${unavailable || !record} @change=${() => update({ count: repeat.count })}>${i18next.t('figure.label.row-by-count')}</label>
|
|
1292
|
+
</div>
|
|
1293
|
+
<p hint>${i18next.t(fits ? 'figure.text.repeat-mode-pitch' : 'figure.text.repeat-mode-count')}</p>
|
|
1294
|
+
<div inline-field><label>${i18next.t('figure.label.size-source')}</label><span value>${parent ?? '—'}</span></div>
|
|
1295
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-axis')}</label>${axes.length ? html `<select aria-label=${i18next.t('figure.label.repeat-axis')} ?disabled=${unavailable} .value=${live(axis)} @change=${(e) => update({ axis: e.target.value.toUpperCase() })}>${axes.map(a => html `<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}</select>` : html `<span value>${axis.toUpperCase()}</span>`}</div>
|
|
1296
|
+
<div inline-field><label>${i18next.t('figure.label.repeat-count')}</label>${!fits && record ? html `<input type="number" min="1" step="1" aria-label=${i18next.t('figure.label.repeat-count')} .value=${live(String(repeat.count))} ?disabled=${unavailable} @change=${(e) => update({ count: e.target.valueAsNumber })}>` : html `<span value>${repeat.count}</span>`}</div>
|
|
1275
1297
|
<div inline-field>
|
|
1276
1298
|
<label>${i18next.t('figure.label.repeat-gap')} <span unit>mm</span></label>
|
|
1277
|
-
|
|
1299
|
+
${fits ? html `<input
|
|
1278
1300
|
type="number"
|
|
1279
1301
|
step="any"
|
|
1280
|
-
aria-label=${
|
|
1281
|
-
.value=${String(pitch ?? '')}
|
|
1282
|
-
?disabled=${unavailable
|
|
1302
|
+
aria-label=${i18next.t('figure.label.repeat-gap')}
|
|
1303
|
+
.value=${live(String(pitch ?? ''))}
|
|
1304
|
+
?disabled=${unavailable}
|
|
1283
1305
|
@change=${(e) => this.setPitch(field, e)}
|
|
1284
|
-
|
|
1306
|
+
/>` : html `<span value>${pitch === undefined ? '—' : Number(pitch.toFixed(3))}</span>`}
|
|
1285
1307
|
</div>
|
|
1286
|
-
<ul row-reading>${rowWordsOf(this.rowNow(shown), (key, params) => i18next.t(key, params)).map(line => html `<li>${line}</li>`)}</ul>
|
|
1287
|
-
<
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
length: length.toLocaleString(),
|
|
1291
|
-
count: repeat.count,
|
|
1292
|
-
longest: longest.toLocaleString(),
|
|
1293
|
-
limit
|
|
1294
|
-
})
|
|
1295
|
-
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1296
|
-
</p>
|
|
1297
|
-
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1308
|
+
<ul row-reading>${rowWordsOf(this.rowNow(shown), (key, params) => i18next.t(key, params)).slice(1).map(line => html `<li>${line}</li>`)}</ul>
|
|
1309
|
+
<button quiet ?disabled=${unavailable || !removable} @click=${() => this.run(() => this.session.command({ kind: 'remove-repeat', part: this.selected }))}>${i18next.t('figure.button.remove-repeat')}</button>
|
|
1310
|
+
<p hint>${i18next.t('figure.text.repeat-remove-explained')}</p>
|
|
1311
|
+
${fits ? this.renderPublicPitch(asset, pitch ?? 0, unavailable) : nothing}
|
|
1298
1312
|
</section>`;
|
|
1299
1313
|
}
|
|
1300
1314
|
/** The selected row as it is drawn now, read by figure-model: nothing when it cannot be read. */
|
|
@@ -1610,8 +1624,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1610
1624
|
/** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
|
|
1611
1625
|
renderTab(tab, icon, label) {
|
|
1612
1626
|
/* Its accessible name is the name it shows (designer 2026-09-27); the longer title stays as its tooltip. */
|
|
1613
|
-
const name = i18next.t('figure.label.tab-short-' + tab);
|
|
1614
|
-
return html `<button tab ?on=${this.tab === tab} title=${label} aria-label=${name} @click=${() => (this.tab = tab)}>
|
|
1627
|
+
const name = i18next.t(tab === 'response' && this.selected ? 'figure.label.tab-arrangement' : 'figure.label.tab-short-' + tab);
|
|
1628
|
+
return html `<button tab ?on=${this.tab === tab} title=${tab === 'response' && this.selected ? name : label} aria-label=${name} @click=${() => (this.tab = tab)}>
|
|
1615
1629
|
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span>
|
|
1616
1630
|
</button>`;
|
|
1617
1631
|
}
|
|
@@ -1741,8 +1755,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1741
1755
|
return nothing;
|
|
1742
1756
|
return html `<div subject>
|
|
1743
1757
|
<div subject-summary>
|
|
1744
|
-
|
|
1745
|
-
${summary.
|
|
1758
|
+
<b>${ownerOf(this.selected)}</b>
|
|
1759
|
+
${summary.kind ? html `<small>${summary.kind}</small>` : nothing}
|
|
1746
1760
|
</div>
|
|
1747
1761
|
</div>`;
|
|
1748
1762
|
}
|