@things-factory/figure-ui 10.1.79 → 10.1.82
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/figure-assistant.ts +2 -2
- package/client/graphql/index.ts +99 -10
- package/client/graphql/reached.ts +25 -0
- package/client/graphql/store.ts +27 -4
- package/client/modeller/ai-proposal.ts +74 -9
- package/client/modeller/align.ts +2 -3
- package/client/modeller/canvas.ts +18 -5
- package/client/modeller/command-preview.ts +113 -0
- package/client/modeller/editor.ts +826 -54
- package/client/modeller/fields.ts +12 -4
- package/client/modeller/figure-facts.ts +2 -2
- package/client/modeller/figure-first-save.ts +36 -14
- package/client/modeller/figure-history-panel.ts +133 -14
- package/client/modeller/figure-links.ts +385 -68
- package/client/modeller/figure-parts.ts +56 -2
- package/client/modeller/figure-preview.ts +19 -1
- package/client/modeller/figure-properties.ts +3 -3
- package/client/modeller/figure-revert-choice.ts +49 -0
- package/client/modeller/first-save.ts +16 -0
- package/client/modeller/gizmo.ts +0 -16
- package/client/modeller/links.ts +142 -15
- package/client/modeller/motion-ends.ts +83 -0
- package/client/modeller/occupancy.ts +28 -2
- package/client/modeller/product-session.ts +154 -18
- package/client/modeller/refusals.ts +15 -0
- package/client/modeller/revert-request.ts +26 -0
- package/client/modeller/side-panel-styles.ts +12 -0
- package/client/modeller/stage-model.ts +25 -3
- package/client/pages/figure-modeller-page.ts +68 -29
- package/dist-client/figure-assistant.js +2 -2
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/index.d.ts +39 -2
- package/dist-client/graphql/index.js +63 -9
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/reached.d.ts +10 -0
- package/dist-client/graphql/reached.js +28 -0
- package/dist-client/graphql/reached.js.map +1 -0
- package/dist-client/graphql/store.js +27 -4
- package/dist-client/graphql/store.js.map +1 -1
- package/dist-client/modeller/ai-proposal.d.ts +31 -2
- package/dist-client/modeller/ai-proposal.js +65 -6
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/align.d.ts +1 -1
- package/dist-client/modeller/align.js +0 -2
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +11 -0
- package/dist-client/modeller/canvas.js +33 -4
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview.d.ts +35 -0
- package/dist-client/modeller/command-preview.js +95 -0
- package/dist-client/modeller/command-preview.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +113 -1
- package/dist-client/modeller/editor.js +850 -50
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fields.d.ts +3 -2
- package/dist-client/modeller/fields.js +10 -3
- package/dist-client/modeller/fields.js.map +1 -1
- package/dist-client/modeller/figure-facts.js +2 -2
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +3 -4
- package/dist-client/modeller/figure-first-save.js +41 -14
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +31 -0
- package/dist-client/modeller/figure-history-panel.js +130 -14
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +51 -1
- package/dist-client/modeller/figure-links.js +392 -72
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +7 -0
- package/dist-client/modeller/figure-parts.js +55 -2
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +6 -0
- package/dist-client/modeller/figure-preview.js +22 -1
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +3 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-revert-choice.d.ts +17 -0
- package/dist-client/modeller/figure-revert-choice.js +59 -0
- package/dist-client/modeller/figure-revert-choice.js.map +1 -0
- package/dist-client/modeller/first-save.d.ts +7 -0
- package/dist-client/modeller/first-save.js +17 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +0 -6
- package/dist-client/modeller/gizmo.js +0 -18
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +49 -4
- package/dist-client/modeller/links.js +141 -15
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +12 -0
- package/dist-client/modeller/motion-ends.js +65 -0
- package/dist-client/modeller/motion-ends.js.map +1 -0
- package/dist-client/modeller/occupancy.d.ts +14 -1
- package/dist-client/modeller/occupancy.js +14 -1
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +43 -2
- package/dist-client/modeller/product-session.js +146 -12
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +13 -0
- package/dist-client/modeller/refusals.js +16 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/revert-request.d.ts +16 -0
- package/dist-client/modeller/revert-request.js +11 -0
- package/dist-client/modeller/revert-request.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.js +12 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +18 -2
- package/dist-client/modeller/stage-model.js +21 -3
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +8 -1
- package/dist-client/pages/figure-modeller-page.js +72 -34
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/ai-proposal.test.ts +2 -2
- package/test/align.test.ts +50 -5
- package/test/browser/acceptance-page.js +187 -0
- package/test/browser/build.mjs +5 -1
- package/test/browser/revert-page.js +47 -0
- package/test/browser/serve-acceptance.mjs +85 -0
- package/test/browser/serve-revert.mjs +119 -0
- package/test/clip-ai-proposal.test.ts +104 -0
- package/test/clip-consent.test.ts +88 -0
- package/test/command-preview.test.ts +288 -0
- package/test/fields.test.ts +4 -3
- package/test/figure-assistant-surface.test.ts +4 -1
- package/test/first-minute.test.ts +96 -0
- package/test/first-save-suggest.test.ts +42 -0
- package/test/gizmo.test.ts +20 -16
- package/test/joint-fan.test.ts +2 -1
- package/test/layout.test.ts +9 -2
- package/test/links.test.ts +7 -4
- package/test/proposal-record.test.ts +126 -0
- package/test/release-entry.test.ts +28 -0
- package/test/release-panel.test.ts +3 -2
- package/test/resize-with-placement.test.ts +225 -0
- package/test/revert-choice.test.ts +39 -0
- package/test/revert-keeping.test.ts +102 -0
- package/test/save-landed.test.ts +198 -0
- package/translations/en.json +222 -10
- package/translations/ja.json +223 -11
- package/translations/ko.json +223 -11
- package/translations/ms.json +222 -10
- package/translations/zh.json +223 -11
|
@@ -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 } 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, positionsWorkedOut } from './links.js';
|
|
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';
|
|
8
8
|
import { SidePanelStyles } from './side-panel-styles.js';
|
|
9
9
|
import { HelpToggles } from './help.js';
|
|
10
10
|
const FACES = Object.keys(FIGURE_FACES);
|
|
@@ -17,7 +17,8 @@ const freshFasten = () => ({
|
|
|
17
17
|
gap: 0,
|
|
18
18
|
twist: 0,
|
|
19
19
|
align: {},
|
|
20
|
-
alignMm: {}
|
|
20
|
+
alignMm: {},
|
|
21
|
+
alignInput: {}
|
|
21
22
|
});
|
|
22
23
|
const freshMotion = () => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 });
|
|
23
24
|
const freshRow = () => ({ axis: '', pitch: 200, fills: true, count: 5 });
|
|
@@ -44,6 +45,23 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
44
45
|
this.locked = false;
|
|
45
46
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
46
47
|
this.section = 'shape';
|
|
48
|
+
/** What the fastening command would do, in words (the editor works it out: `command-preview.ts`). */
|
|
49
|
+
this.previewWords = [];
|
|
50
|
+
/** Where the selected part's motion takes it, at both ends of its range (the editor draws them on the canvas). */
|
|
51
|
+
this.motionEnds = null;
|
|
52
|
+
/** Past the edge of the face it is fastened to, or parts fastened to it past theirs (ADR-0097 §5.5, Q3). */
|
|
53
|
+
this.overhangWords = [];
|
|
54
|
+
/** Wider than the face it is fastened to: how far past each edge, and whether off to one side -- a fact, not a warning. */
|
|
55
|
+
this.overhangNote = [];
|
|
56
|
+
/** What taking this part off would do, said beside the buttons that do it (worked out on a copy by the editor). */
|
|
57
|
+
this.detachWords = [];
|
|
58
|
+
this.told = '';
|
|
59
|
+
/* Two forms describe a command here: the fastening and the row. The one being worked on is the one seen. */
|
|
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 = {};
|
|
63
|
+
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
64
|
+
this.spotlit = false;
|
|
47
65
|
this.fasten = freshFasten();
|
|
48
66
|
this.motion = freshMotion();
|
|
49
67
|
this.place = freshPlace();
|
|
@@ -68,6 +86,30 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
68
86
|
b[bound] {
|
|
69
87
|
font-weight: 600;
|
|
70
88
|
}
|
|
89
|
+
/* What each direction does now: read-only rows. */
|
|
90
|
+
div[resize-now] { display: grid; gap: 4px; margin: 12px 0; }
|
|
91
|
+
div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
|
|
92
|
+
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
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; }
|
|
98
|
+
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
|
+
div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
|
|
100
|
+
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
101
|
+
p[fasten-mode][editing] { color: var(--md-sys-color-primary, #477b9e); }
|
|
102
|
+
section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
|
|
103
|
+
details { margin-top: 14px; }
|
|
104
|
+
summary { cursor: pointer; padding: 8px 0; }
|
|
105
|
+
/* What the command would do, said beside the result drawn on the canvas. */
|
|
106
|
+
ul[preview-words] { margin: 10px 0; padding: 8px 12px 8px 26px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); display: grid; gap: 4px; }
|
|
107
|
+
div[row-basis] { display: grid; gap: 4px; margin: 8px 0; }
|
|
108
|
+
div[row-basis] label { display: flex; align-items: center; gap: 6px; }
|
|
109
|
+
div[overhang] { display: grid; gap: 6px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--md-sys-color-outline-variant, #8884); }
|
|
110
|
+
div[overhang] p { margin: 0; }
|
|
111
|
+
ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; font-size: 0.8rem; opacity: 0.85; }
|
|
112
|
+
span[fasten-summary] { opacity: 0.8; }
|
|
71
113
|
/* The difference a following measure keeps: under its choice, on the same line as its words. */
|
|
72
114
|
label[inline-plus] {
|
|
73
115
|
display: flex;
|
|
@@ -79,12 +121,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
79
121
|
min-width: 0;
|
|
80
122
|
}
|
|
81
123
|
`]; }
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*/
|
|
86
|
-
willUpdate(changed) {
|
|
87
|
-
if (changed.has('partId') && changed.get('partId') !== undefined) {
|
|
124
|
+
willUpdate(_changed) {
|
|
125
|
+
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
126
|
+
this.loadedFor = this.partId;
|
|
88
127
|
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
89
128
|
const own = this.asset && this.partId ? fasteningOf(this.asset, this.partId) : null;
|
|
90
129
|
this.fasten = own ? { ...freshFasten(), ...own } : freshFasten();
|
|
@@ -94,8 +133,29 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
94
133
|
this.editingMotion = false;
|
|
95
134
|
}
|
|
96
135
|
}
|
|
136
|
+
updated() {
|
|
137
|
+
/* Only the section that holds the fastening form speaks of it: the panel has one of these per section, and the
|
|
138
|
+
others' silence cleared what it showed (2026-09-27, :3391). */
|
|
139
|
+
if (this.section !== 'shape' && !this.told)
|
|
140
|
+
return;
|
|
141
|
+
const said = JSON.stringify(this.candidate ?? null);
|
|
142
|
+
if (said === this.told)
|
|
143
|
+
return;
|
|
144
|
+
this.told = said;
|
|
145
|
+
this.dispatchEvent(new CustomEvent('preview-command', { detail: this.candidate ?? null, bubbles: true, composed: true }));
|
|
146
|
+
}
|
|
147
|
+
disconnectedCallback() {
|
|
148
|
+
super.disconnectedCallback();
|
|
149
|
+
/* The panel left: what it showed goes with it. */
|
|
150
|
+
if (this.told && this.told !== 'null')
|
|
151
|
+
this.dispatchEvent(new CustomEvent('preview-command', { detail: null, bubbles: true, composed: true }));
|
|
152
|
+
this.told = '';
|
|
153
|
+
}
|
|
97
154
|
render() {
|
|
98
155
|
const asset = this.asset;
|
|
156
|
+
this.candidate = undefined;
|
|
157
|
+
this.fastenCandidate = undefined;
|
|
158
|
+
this.rowCandidate = undefined;
|
|
99
159
|
if (!asset || !this.partId)
|
|
100
160
|
return nothing;
|
|
101
161
|
if (this.section === 'motion')
|
|
@@ -104,18 +164,20 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
104
164
|
if (this.section === 'places')
|
|
105
165
|
return html `<section><h3>${i18next.t('figure.label.places-on-part', { part: this.partId })}${this.help.button('figure.text.place-explained')}</h3>${this.help.hint('figure.text.place-explained')}
|
|
106
166
|
${placeFacesOf(asset, this.partId).length ? this.renderPlaces(asset) : html `<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`;
|
|
107
|
-
|
|
167
|
+
const shown = html `
|
|
108
168
|
<section>
|
|
109
|
-
<h3>${i18next.t('figure.label.
|
|
169
|
+
<h3>${i18next.t('figure.label.resize-intent')}</h3>
|
|
110
170
|
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html `<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
111
171
|
</section>
|
|
112
|
-
<section>
|
|
172
|
+
<section fastening ?spotlit=${this.spotlit} @focusin=${() => (this.working = 'fasten')}>
|
|
113
173
|
<h3>${i18next.t('figure.label.placement')}${this.help.button('figure.text.fastening-explained')}</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
114
174
|
${this.renderPositions(asset)}
|
|
115
175
|
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html `<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
116
176
|
</section>
|
|
117
177
|
${this.renderRow(asset)}
|
|
118
178
|
`;
|
|
179
|
+
this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate;
|
|
180
|
+
return shown;
|
|
119
181
|
}
|
|
120
182
|
/** Where things are held on this part: a seat things rest on, or a grip things are held in. */
|
|
121
183
|
renderPlaces(asset) {
|
|
@@ -183,16 +245,93 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
183
245
|
this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capabilities', names } }, bubbles: true, composed: true }));
|
|
184
246
|
}
|
|
185
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
|
+
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
|
+
const round = editable.length === 2;
|
|
259
|
+
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
|
+
};
|
|
186
272
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one));
|
|
187
|
-
const round = followableAxes(asset, this.partId).length === 2;
|
|
188
273
|
/* One step for the whole part (figure-model follow-size): the size is declared from the parts if there is none. */
|
|
189
|
-
const followsAll =
|
|
274
|
+
const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance');
|
|
190
275
|
const whole = followsAll
|
|
191
276
|
? nothing
|
|
192
277
|
: html `<button act follow-size ?disabled=${this.locked} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
193
278
|
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
194
279
|
</button>`;
|
|
195
|
-
|
|
280
|
+
const held = plan.refusal?.code === 'HELD';
|
|
281
|
+
return html `<p hint>${i18next.t('figure.text.resize-intent')}</p>
|
|
282
|
+
${asset.sizing === 'uniform' ? html `<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
|
|
283
|
+
<div resize-now>${editable.map(axis => {
|
|
284
|
+
const now = followOf(asset, this.partId, axis);
|
|
285
|
+
const following = now.of === 'instance' && now.axis === axis;
|
|
286
|
+
const custom = now.of !== 'own' && !following;
|
|
287
|
+
const mm = measureNow(asset, this.partId, axis);
|
|
288
|
+
return html `<div row>
|
|
289
|
+
<b>${word(axis)}</b>
|
|
290
|
+
<span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
|
|
291
|
+
<span>${i18next.t(custom ? 'figure.label.resize-custom' : following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
|
|
292
|
+
${following
|
|
293
|
+
? html `<button link resize-unfollow=${axis} ?disabled=${this.locked}
|
|
294
|
+
@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
|
+
this.follow(asset, axis, 'own', now);
|
|
298
|
+
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
299
|
+
: nothing}
|
|
300
|
+
</div>`;
|
|
301
|
+
})}</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 => {
|
|
196
335
|
const now = followOf(asset, this.partId, axis);
|
|
197
336
|
const value = choiceOf(now);
|
|
198
337
|
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
@@ -226,7 +365,65 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
226
365
|
</label>`
|
|
227
366
|
: nothing}
|
|
228
367
|
</div>`;
|
|
229
|
-
})}
|
|
368
|
+
})}</details>`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* "Open attachment settings": the fastening this part has now -- its face, what it is on, the gap -- in the form below,
|
|
372
|
+
* brought into view with the focus on it. The form is loaded again first: the part was fastened by "follow size,
|
|
373
|
+
* attached" while it was selected, so the form still held what it had before (designer 2026-09-27: it opened the
|
|
374
|
+
* holding places instead).
|
|
375
|
+
*/
|
|
376
|
+
/** The fastening's details open, focus in them: where the gap and the place across the face are changed. */
|
|
377
|
+
async openDetail() {
|
|
378
|
+
const detail = this.renderRoot.querySelector('details[fasten-detail]');
|
|
379
|
+
if (!detail)
|
|
380
|
+
return;
|
|
381
|
+
detail.open = true;
|
|
382
|
+
this.working = 'fasten';
|
|
383
|
+
await this.updateComplete;
|
|
384
|
+
detail.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
385
|
+
detail.querySelector('select, input')?.focus({ preventScroll: true });
|
|
386
|
+
}
|
|
387
|
+
/** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
|
|
388
|
+
async showFastening() {
|
|
389
|
+
if (this.asset)
|
|
390
|
+
await this.openFastening(this.asset);
|
|
391
|
+
}
|
|
392
|
+
async openFastening(asset) {
|
|
393
|
+
const own = fasteningOf(asset, this.partId);
|
|
394
|
+
this.fasten = own ? { ...freshFasten(), ...own } : freshFasten();
|
|
395
|
+
this.spotlit = true;
|
|
396
|
+
await this.updateComplete;
|
|
397
|
+
const section = this.renderRoot.querySelector('section[fastening]');
|
|
398
|
+
section?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
399
|
+
(section?.querySelector('select, input') ?? section?.querySelector('button'))?.focus({ preventScroll: true });
|
|
400
|
+
setTimeout(() => (this.spotlit = false), 1600);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* What holds the part, said as its state -- 「아랫면을 바닥에 유지 중」 -- with the ways to change it: open the fastening,
|
|
404
|
+
* or take it off. Right after "follow size, attached" this is the result, not a refusal (designer 2026-09-27).
|
|
405
|
+
*/
|
|
406
|
+
renderHeld(asset) {
|
|
407
|
+
const joint = (asset.joints ?? []).find(j => j.body1.part === this.partId);
|
|
408
|
+
const face = joint ? i18next.t(`figure.label.face-${joint.body1.face}`) : '';
|
|
409
|
+
const gap = typeof joint?.origin?.gap === 'number' ? joint.origin.gap : 0;
|
|
410
|
+
const where = !joint
|
|
411
|
+
? i18next.t('figure.text.held-by-rule')
|
|
412
|
+
: 'plane' in joint.body0
|
|
413
|
+
? i18next.t('figure.text.held-on-plane', { face, place: i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`), gap: String(gap) })
|
|
414
|
+
: i18next.t('figure.text.held-on-part', { face, other: joint.body0.part, otherFace: i18next.t(`figure.label.face-${joint.body0.face}`) });
|
|
415
|
+
return html `<div resize-held>
|
|
416
|
+
<p><md-icon>link</md-icon> ${where}</p>
|
|
417
|
+
<button act open-fastening @click=${() => this.openFastening(asset)}>
|
|
418
|
+
${i18next.t('figure.button.open-fastening')}
|
|
419
|
+
</button>
|
|
420
|
+
${joint
|
|
421
|
+
? html `<button act detach-held ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
422
|
+
${i18next.t('figure.button.detach-held')}
|
|
423
|
+
</button>
|
|
424
|
+
${this.renderDetachWords()}`
|
|
425
|
+
: nothing}
|
|
426
|
+
</div>`;
|
|
230
427
|
}
|
|
231
428
|
/** The same source, the same share, a new difference: a leaf the opening less its gaps (link-dimension's `plus`). */
|
|
232
429
|
followPlus(axis, now, plus) {
|
|
@@ -286,13 +483,15 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
286
483
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
|
|
287
484
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0];
|
|
288
485
|
const aim = f.toPlane ? 'plane' : f.target ? { part: f.target, face: targetFace } : undefined;
|
|
289
|
-
this.fasten = { ...f, ...(aim ? placeKeptOf(asset, this.partId, aim, !!f.twist) : { align: {}, alignMm: {} }) };
|
|
486
|
+
this.fasten = { ...f, ...(aim ? placeKeptOf(asset, this.partId, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) };
|
|
290
487
|
}
|
|
291
488
|
renderFastening(asset) {
|
|
292
489
|
const held = heldBy(asset, this.partId);
|
|
293
490
|
const f = this.fasten;
|
|
294
491
|
/* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */
|
|
295
|
-
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one)
|
|
492
|
+
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one));
|
|
493
|
+
/* Shown, not dropped, and not choosable, with the reason (ADR-0097 §5.5): fastening it would close a loop. */
|
|
494
|
+
const loops = (one) => hangsFrom(asset, one, this.partId);
|
|
296
495
|
/* The faces are the ones each shape declares: a part is fastened by one of its outer faces, to any face of the target. */
|
|
297
496
|
/* On the mounting plane only the faces it takes: a bottom or a top on the floor, a back on a wall. */
|
|
298
497
|
const plane = mountingPlaneOf(asset);
|
|
@@ -305,15 +504,64 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
305
504
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
|
|
306
505
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0];
|
|
307
506
|
const aim = f.toPlane ? 'plane' : { part: f.target, face: targetFace };
|
|
308
|
-
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist) : undefined;
|
|
507
|
+
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist, f.alignInput) : undefined;
|
|
508
|
+
const action = f.toPlane || f.target
|
|
509
|
+
? {
|
|
510
|
+
kind: 'attach',
|
|
511
|
+
part: this.partId,
|
|
512
|
+
face: myFace,
|
|
513
|
+
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
514
|
+
...(f.toPlane ? {} : { facing: f.facing }),
|
|
515
|
+
...(f.gap ? { gap: f.gap } : {}),
|
|
516
|
+
twist: f.twist,
|
|
517
|
+
...(align ? { align } : {}),
|
|
518
|
+
replace: !!held
|
|
519
|
+
}
|
|
520
|
+
: undefined;
|
|
521
|
+
if (action)
|
|
522
|
+
this.fastenCandidate = { action, ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } }) };
|
|
523
|
+
/* What the details hold that is not their default, and where on the face it sits: said while they are folded. */
|
|
524
|
+
const detail = [
|
|
525
|
+
...(f.facing === 'flush' && !f.toPlane ? [i18next.t('figure.label.facing-flush')] : []),
|
|
526
|
+
...Object.entries(align ?? {}).map(([axis, one]) => one === 'min' || one === 'max'
|
|
527
|
+
? i18next.t(`figure.text.fasten-summary-edge-${one}`, { axis: axis.toUpperCase() })
|
|
528
|
+
: typeof one.mm === 'number'
|
|
529
|
+
? i18next.t('figure.text.fasten-summary-off-centre', { axis: axis.toUpperCase(), mm: String(Math.round(one.mm * 10) / 10) })
|
|
530
|
+
: i18next.t('figure.text.fasten-summary-follows', { axis: axis.toUpperCase() })),
|
|
531
|
+
...(f.gap ? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })] : []),
|
|
532
|
+
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
533
|
+
];
|
|
534
|
+
/*
|
|
535
|
+
Editing the fastening it has, or fastening it anew: said, with the relation named, so the form is not taken for a
|
|
536
|
+
blank new one while it holds the current (designer 2026-09-27). Applied unchanged, the same fastening is written
|
|
537
|
+
again, fields and all (resize-with-placement.test).
|
|
538
|
+
*/
|
|
539
|
+
const current = asset.joints?.find(j => j.body1.part === this.partId);
|
|
540
|
+
const editing = current
|
|
541
|
+
? i18next.t('figure.text.fasten-editing', {
|
|
542
|
+
face: i18next.t(`figure.label.face-${current.body1.face}`),
|
|
543
|
+
target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
|
|
544
|
+
})
|
|
545
|
+
: i18next.t('figure.text.fasten-new');
|
|
309
546
|
return html `
|
|
547
|
+
<p fasten-mode ?editing=${!!current}>${editing}</p>
|
|
548
|
+
${this.overhangNote.length ? html `<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
549
|
+
${this.overhangWords.length
|
|
550
|
+
? html `<div overhang role="status">
|
|
551
|
+
${this.overhangWords.map(line => html `<p>${line}</p>`)}
|
|
552
|
+
${current
|
|
553
|
+
? html `<button act @click=${() => this.openDetail()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>`
|
|
554
|
+
: nothing}
|
|
555
|
+
</div>`
|
|
556
|
+
: nothing}
|
|
310
557
|
${held
|
|
311
558
|
? html `<div pair>
|
|
312
559
|
<span>${i18next.t('figure.label.fastened-to')}: <b bound>${'plane' in held ? i18next.t('figure.label.mounting-plane') : held.part}</b></span>
|
|
313
560
|
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
314
561
|
<md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
|
|
315
562
|
</button>
|
|
316
|
-
</div
|
|
563
|
+
</div>
|
|
564
|
+
${this.renderDetachWords()}`
|
|
317
565
|
: nothing}
|
|
318
566
|
<div form>
|
|
319
567
|
<span>${i18next.t('figure.label.my-face')}</span>
|
|
@@ -331,44 +579,43 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
331
579
|
<span>${i18next.t('figure.label.target-part')}</span>
|
|
332
580
|
<select @change=${(e) => this.aimAt(asset, { ...f, target: e.target.value })}>
|
|
333
581
|
<option value="" ?selected=${!f.target}>—</option>
|
|
334
|
-
${targets.map(one =>
|
|
582
|
+
${targets.map(one => loops(one)
|
|
583
|
+
? html `<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
584
|
+
: html `<option value=${one} ?selected=${one === f.target}>${one}</option>`)}
|
|
335
585
|
</select>
|
|
336
586
|
<span>${i18next.t('figure.label.target-face')}</span>
|
|
337
587
|
<select @change=${(e) => this.aimAt(asset, { ...f, targetFace: e.target.value })}>
|
|
338
588
|
${theirFaces.map(face => html `<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
339
589
|
</select>
|
|
340
|
-
<span>${i18next.t('figure.label.facing-meet')}</span>
|
|
341
|
-
<select @change=${(e) => (this.fasten = { ...f, facing: e.target.value })}>
|
|
342
|
-
<option value="meet" ?selected=${f.facing === 'meet'}>${i18next.t('figure.label.facing-meet')}</option>
|
|
343
|
-
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
344
|
-
</select>
|
|
345
590
|
</div>`}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
<md-icon>link</md-icon>${i18next.t('figure.button.fasten')}
|
|
591
|
+
<details fasten-detail>
|
|
592
|
+
<summary>${i18next.t('figure.label.fasten-detail')} · <span fasten-summary>${detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')}</span></summary>
|
|
593
|
+
${f.toPlane
|
|
594
|
+
? nothing
|
|
595
|
+
: html `<div form>
|
|
596
|
+
<span>${i18next.t('figure.label.facing')}</span>
|
|
597
|
+
<select @change=${(e) => (this.fasten = { ...f, facing: e.target.value })}>
|
|
598
|
+
<option value="meet" ?selected=${f.facing === 'meet'}>${i18next.t('figure.label.facing-meet')}</option>
|
|
599
|
+
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
600
|
+
</select>
|
|
601
|
+
</div>`}
|
|
602
|
+
${this.renderAlignment(f.toPlane || f.target ? alignAxesOf(asset, this.partId, aim, !!f.twist) : [])}
|
|
603
|
+
<div form>
|
|
604
|
+
<span>${i18next.t('figure.label.gap')}</span>
|
|
605
|
+
<input type="number" step="any" .value=${String(f.gap)} @change=${(e) => (this.fasten = { ...f, gap: Number(e.target.value) || 0 })} />
|
|
606
|
+
<span>${i18next.t('figure.label.twist')}</span>
|
|
607
|
+
<input type="number" step="any" .value=${String(f.twist)} @change=${(e) => (this.fasten = { ...f, twist: Number(e.target.value) || 0 })} />
|
|
608
|
+
</div>
|
|
609
|
+
</details>
|
|
610
|
+
${action && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
|
|
611
|
+
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
612
|
+
<md-icon>link</md-icon>${i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')}
|
|
369
613
|
</button>
|
|
370
614
|
`;
|
|
371
615
|
}
|
|
616
|
+
renderDetachWords() {
|
|
617
|
+
return this.detachWords.length ? html `<ul detach-words>${this.detachWords.map(line => html `<li>${line}</li>`)}</ul>` : nothing;
|
|
618
|
+
}
|
|
372
619
|
/**
|
|
373
620
|
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
374
621
|
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
@@ -387,6 +634,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
387
634
|
? html `<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
388
635
|
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>`
|
|
389
636
|
: nothing}
|
|
637
|
+
${f.alignInput?.[axis]
|
|
638
|
+
? html `<option value="input" ?selected=${how === 'input'}>${i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis], key => i18next.t(key)) })}</option>`
|
|
639
|
+
: nothing}
|
|
390
640
|
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
391
641
|
${this.asset && hasInstanceSize(this.asset, axis)
|
|
392
642
|
? html `<option value="share" ?selected=${how === 'share'}>${i18next.t('figure.label.align-share')}</option>`
|
|
@@ -403,38 +653,59 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
403
653
|
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
404
654
|
* (the editor's) shows it and takes it away.
|
|
405
655
|
*/
|
|
656
|
+
renderPreviewWords() {
|
|
657
|
+
return this.previewWords.length ? html `<ul preview-words role="status">${this.previewWords.map(line => html `<li>${line}</li>`)}</ul>` : nothing;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* A row (ADR-0097 §5.5): said as what it makes, "arranged in a row". Where it cannot be made here, why, rather than
|
|
661
|
+
* nothing. Filling the length, the centre spacing is the author's; a count of their own is spread evenly over the
|
|
662
|
+
* length -- figure-model draws it so, and the form does not ask for a spacing it would not use.
|
|
663
|
+
*/
|
|
406
664
|
renderRow(asset) {
|
|
407
|
-
|
|
408
|
-
if (!axes.length)
|
|
665
|
+
if (rowOf(asset, this.partId))
|
|
409
666
|
return nothing;
|
|
667
|
+
const blocked = rowBlockOf(asset, this.partId);
|
|
668
|
+
const axes = rowAxesOf(asset, this.partId);
|
|
410
669
|
const r = this.row;
|
|
670
|
+
if (blocked || !axes.length)
|
|
671
|
+
return html `<section row>
|
|
672
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
673
|
+
<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>
|
|
674
|
+
${blocked === 'unfastened'
|
|
675
|
+
? html `<button act @click=${() => this.openFastening(asset)}><md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}</button>`
|
|
676
|
+
: nothing}
|
|
677
|
+
</section>`;
|
|
411
678
|
const axis = axes.includes(r.axis) ? r.axis : axes[0];
|
|
412
|
-
const ok = r.pitch > 0
|
|
413
|
-
|
|
679
|
+
const ok = r.fills ? r.pitch > 0 : Number.isSafeInteger(r.count) && r.count >= 1;
|
|
680
|
+
const action = ok
|
|
681
|
+
? { kind: 'make-repeat', part: this.partId, axis, pitch: r.fills ? r.pitch : Math.max(r.pitch, 1), ...(r.fills ? {} : { count: r.count }) }
|
|
682
|
+
: undefined;
|
|
683
|
+
this.rowCandidate = action;
|
|
684
|
+
const set = (next) => ((this.row = next), (this.working = 'row'));
|
|
685
|
+
return html `<section row @focusin=${() => (this.working = 'row')}>
|
|
414
686
|
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
415
687
|
<div form>
|
|
416
688
|
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
417
|
-
<select @change=${(e) => (
|
|
689
|
+
<select @change=${(e) => set({ ...r, axis: e.target.value })}>
|
|
418
690
|
${axes.map(a => html `<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}
|
|
419
691
|
</select>
|
|
420
|
-
<span>${i18next.t('figure.label.repeat-pitch')} (mm)</span>
|
|
421
|
-
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e) => (this.row = { ...r, pitch: Number(e.target.value) })} />
|
|
422
692
|
</div>
|
|
423
|
-
<label
|
|
424
|
-
<input type="
|
|
425
|
-
|
|
426
|
-
</
|
|
693
|
+
<div row-basis role="radiogroup" aria-label=${i18next.t('figure.label.row-basis')}>
|
|
694
|
+
<label><input type="radio" name="row-basis" .checked=${r.fills} @change=${() => set({ ...r, fills: true })} />${i18next.t('figure.label.repeat-fills')}</label>
|
|
695
|
+
<label><input type="radio" name="row-basis" .checked=${!r.fills} @change=${() => set({ ...r, fills: false })} />${i18next.t('figure.label.row-by-count')}</label>
|
|
696
|
+
</div>
|
|
427
697
|
${r.fills
|
|
428
|
-
?
|
|
698
|
+
? html `<div form>
|
|
699
|
+
<span>${i18next.t('figure.label.repeat-pitch')} · mm</span>
|
|
700
|
+
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e) => set({ ...r, pitch: Number(e.target.value) })} />
|
|
701
|
+
</div>`
|
|
429
702
|
: html `<div form>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
@click=${() => this.command({ kind: 'make-repeat', part: this.partId, axis, pitch: r.pitch, ...(r.fills ? {} : { count: r.count }) })}
|
|
437
|
-
>
|
|
703
|
+
<span>${i18next.t('figure.label.repeat-count')}</span>
|
|
704
|
+
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e) => set({ ...r, count: Number(e.target.value) })} />
|
|
705
|
+
</div>
|
|
706
|
+
<p hint>${i18next.t('figure.text.row-count-spreads')}</p>`}
|
|
707
|
+
${action && this.working === 'row' ? this.renderPreviewWords() : nothing}
|
|
708
|
+
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
438
709
|
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
439
710
|
</button>
|
|
440
711
|
</section>`;
|
|
@@ -486,7 +757,21 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
486
757
|
const now = motionOf(asset, this.partId);
|
|
487
758
|
const joint = asset.joints?.find(j => j.body1.part === this.partId);
|
|
488
759
|
if (now && !(this.editingMotion && joint)) {
|
|
489
|
-
|
|
760
|
+
/* Which part moves, against what, which way, how far -- by name (ADR-0097 §5.5). */
|
|
761
|
+
const against = joint ? ('plane' in joint.body0 ? i18next.t('figure.label.mounting-plane') : joint.body0.part) : '';
|
|
762
|
+
const ends = this.motionEnds;
|
|
763
|
+
return html `<p motion-summary>
|
|
764
|
+
${i18next.t('figure.text.motion-summary', {
|
|
765
|
+
part: this.partId,
|
|
766
|
+
against,
|
|
767
|
+
kind: i18next.t('figure.label.motion-' + now.kind),
|
|
768
|
+
axis: String(now.axis).toUpperCase()
|
|
769
|
+
})}
|
|
770
|
+
</p>
|
|
771
|
+
${ends
|
|
772
|
+
? html `<p hint motion-ends>${i18next.t('figure.text.motion-ends-shown', { min: String(ends.min), max: String(ends.max), unit: ends.unit })}</p>`
|
|
773
|
+
: nothing}
|
|
774
|
+
<div pair>
|
|
490
775
|
<span>
|
|
491
776
|
${i18next.t('figure.label.motion-' + now.kind)} · ${String(now.axis).toUpperCase()}
|
|
492
777
|
${now.travel
|
|
@@ -501,7 +786,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
501
786
|
<button
|
|
502
787
|
act
|
|
503
788
|
?disabled=${this.locked || !joint}
|
|
504
|
-
@click=${() => joint && this.command({ kind: 'set-joint', joint:
|
|
789
|
+
@click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}
|
|
505
790
|
>
|
|
506
791
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
507
792
|
</button>
|
|
@@ -521,7 +806,10 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
521
806
|
}
|
|
522
807
|
/* A joint needs a parent (URDF): a part fastened to nothing is fastened first. */
|
|
523
808
|
if (!joint)
|
|
524
|
-
return html `<p hint>${i18next.t('figure.text.fasten-before-moving')}</p
|
|
809
|
+
return html `<p hint>${i18next.t('figure.text.fasten-before-moving')}</p>
|
|
810
|
+
<button act go-fasten @click=${() => this.dispatchEvent(new CustomEvent('go-fasten', { bubbles: true, composed: true }))}>
|
|
811
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}
|
|
812
|
+
</button>`;
|
|
525
813
|
const m = this.motion;
|
|
526
814
|
const unit = m.kind === 'slide' ? 'mm' : 'deg';
|
|
527
815
|
/* A follower follows a joint that moves the same way; a choice the kind no longer allows is dropped. */
|
|
@@ -622,6 +910,38 @@ __decorate([
|
|
|
622
910
|
property(),
|
|
623
911
|
__metadata("design:type", String)
|
|
624
912
|
], FigureLinks.prototype, "section", void 0);
|
|
913
|
+
__decorate([
|
|
914
|
+
property({ attribute: false }),
|
|
915
|
+
__metadata("design:type", Array)
|
|
916
|
+
], FigureLinks.prototype, "previewWords", void 0);
|
|
917
|
+
__decorate([
|
|
918
|
+
property({ attribute: false }),
|
|
919
|
+
__metadata("design:type", Object)
|
|
920
|
+
], FigureLinks.prototype, "motionEnds", void 0);
|
|
921
|
+
__decorate([
|
|
922
|
+
property({ attribute: false }),
|
|
923
|
+
__metadata("design:type", Array)
|
|
924
|
+
], FigureLinks.prototype, "overhangWords", void 0);
|
|
925
|
+
__decorate([
|
|
926
|
+
property({ attribute: false }),
|
|
927
|
+
__metadata("design:type", Array)
|
|
928
|
+
], FigureLinks.prototype, "overhangNote", void 0);
|
|
929
|
+
__decorate([
|
|
930
|
+
property({ attribute: false }),
|
|
931
|
+
__metadata("design:type", Array)
|
|
932
|
+
], FigureLinks.prototype, "detachWords", void 0);
|
|
933
|
+
__decorate([
|
|
934
|
+
state(),
|
|
935
|
+
__metadata("design:type", String)
|
|
936
|
+
], FigureLinks.prototype, "working", void 0);
|
|
937
|
+
__decorate([
|
|
938
|
+
state(),
|
|
939
|
+
__metadata("design:type", Object)
|
|
940
|
+
], FigureLinks.prototype, "pendingAxes", void 0);
|
|
941
|
+
__decorate([
|
|
942
|
+
state(),
|
|
943
|
+
__metadata("design:type", Object)
|
|
944
|
+
], FigureLinks.prototype, "spotlit", void 0);
|
|
625
945
|
__decorate([
|
|
626
946
|
state(),
|
|
627
947
|
__metadata("design:type", Object)
|