@things-factory/figure-ui 10.1.81 → 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 +5 -5
- 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
|
@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from 'lit'
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
|
-
import { FIGURE_FACES } from '@hatiolab/figure-model'
|
|
7
|
+
import { FIGURE_FACES, planFigureResize } from '@hatiolab/figure-model'
|
|
8
8
|
import type { FigureAsset, FigureAuthoringAction, FigureDimensionSource, FigureFace } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -38,7 +38,13 @@ import {
|
|
|
38
38
|
placeIdFor,
|
|
39
39
|
placesOn,
|
|
40
40
|
rowAxesOf,
|
|
41
|
+
rowBlockOf,
|
|
42
|
+
fixedJointOf,
|
|
43
|
+
rowOf,
|
|
41
44
|
positionsWorkedOut,
|
|
45
|
+
resizeAxesOf,
|
|
46
|
+
fieldWordsOf,
|
|
47
|
+
measureNow,
|
|
42
48
|
type Alignment,
|
|
43
49
|
type Axis,
|
|
44
50
|
type Follow,
|
|
@@ -58,7 +64,8 @@ const freshFasten = () => ({
|
|
|
58
64
|
gap: 0,
|
|
59
65
|
twist: 0,
|
|
60
66
|
align: {} as Partial<Record<Axis, Alignment>>,
|
|
61
|
-
alignMm: {} as Partial<Record<Axis, number
|
|
67
|
+
alignMm: {} as Partial<Record<Axis, number>>,
|
|
68
|
+
alignInput: {} as Partial<Record<Axis, string>>
|
|
62
69
|
})
|
|
63
70
|
const freshMotion = (): MotionForm => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 })
|
|
64
71
|
const freshRow = () => ({ axis: '' as Axis | '', pitch: 200, fills: true, count: 5 })
|
|
@@ -97,6 +104,30 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
97
104
|
b[bound] {
|
|
98
105
|
font-weight: 600;
|
|
99
106
|
}
|
|
107
|
+
/* What each direction does now: read-only rows. */
|
|
108
|
+
div[resize-now] { display: grid; gap: 4px; margin: 12px 0; }
|
|
109
|
+
div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
|
|
110
|
+
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
111
|
+
button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
|
|
112
|
+
/* What will be applied: a pending choice, boxed apart from the rows above. */
|
|
113
|
+
fieldset[resize-pending] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border: 1px dashed var(--border-color, #8888); border-radius: 6px; }
|
|
114
|
+
fieldset[resize-pending] label { display: flex; align-items: center; gap: 6px; }
|
|
115
|
+
fieldset[resize-pending] small { opacity: 0.7; }
|
|
116
|
+
div[resize-held] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
|
|
117
|
+
div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
|
|
118
|
+
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
119
|
+
p[fasten-mode][editing] { color: var(--md-sys-color-primary, #477b9e); }
|
|
120
|
+
section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
|
|
121
|
+
details { margin-top: 14px; }
|
|
122
|
+
summary { cursor: pointer; padding: 8px 0; }
|
|
123
|
+
/* What the command would do, said beside the result drawn on the canvas. */
|
|
124
|
+
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; }
|
|
125
|
+
div[row-basis] { display: grid; gap: 4px; margin: 8px 0; }
|
|
126
|
+
div[row-basis] label { display: flex; align-items: center; gap: 6px; }
|
|
127
|
+
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); }
|
|
128
|
+
div[overhang] p { margin: 0; }
|
|
129
|
+
ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; font-size: 0.8rem; opacity: 0.85; }
|
|
130
|
+
span[fasten-summary] { opacity: 0.8; }
|
|
100
131
|
/* The difference a following measure keeps: under its choice, on the same line as its words. */
|
|
101
132
|
label[inline-plus] {
|
|
102
133
|
display: flex;
|
|
@@ -118,7 +149,31 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
118
149
|
@property({ type: Boolean }) locked = false
|
|
119
150
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
120
151
|
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
152
|
+
/** What the fastening command would do, in words (the editor works it out: `command-preview.ts`). */
|
|
153
|
+
@property({ attribute: false }) previewWords: string[] = []
|
|
154
|
+
/** Where the selected part's motion takes it, at both ends of its range (the editor draws them on the canvas). */
|
|
155
|
+
@property({ attribute: false }) motionEnds: { control: string; min: number; max: number; unit: string } | null = null
|
|
156
|
+
/** Past the edge of the face it is fastened to, or parts fastened to it past theirs (ADR-0097 §5.5, Q3). */
|
|
157
|
+
@property({ attribute: false }) overhangWords: string[] = []
|
|
158
|
+
/** Wider than the face it is fastened to: how far past each edge, and whether off to one side -- a fact, not a warning. */
|
|
159
|
+
@property({ attribute: false }) overhangNote: string[] = []
|
|
160
|
+
/** What taking this part off would do, said beside the buttons that do it (worked out on a copy by the editor). */
|
|
161
|
+
@property({ attribute: false }) detachWords: string[] = []
|
|
162
|
+
/*
|
|
163
|
+
The fastening command the form describes now, and the face it aims at: told to the editor whenever it changes, so
|
|
164
|
+
the result is seen before it is done (ADR-0097 §5.5). Nothing is told while no target is chosen.
|
|
165
|
+
*/
|
|
166
|
+
private candidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
167
|
+
private told = ''
|
|
168
|
+
/* Two forms describe a command here: the fastening and the row. The one being worked on is the one seen. */
|
|
169
|
+
@state() private working: 'fasten' | 'row' = 'fasten'
|
|
170
|
+
private fastenCandidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
171
|
+
private rowCandidate?: FigureAuthoringAction
|
|
121
172
|
|
|
173
|
+
/** The directions ticked for "follow the size, attached", per part, once the author has ticked or fixed any. */
|
|
174
|
+
@state() private pendingAxes: Record<string, Axis[]> = {}
|
|
175
|
+
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
176
|
+
@state() private spotlit = false
|
|
122
177
|
@state() private fasten = freshFasten()
|
|
123
178
|
@state() private motion: MotionForm = freshMotion()
|
|
124
179
|
@state() private place = freshPlace()
|
|
@@ -130,8 +185,12 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
130
185
|
The forms describe the next command for the selected part, so another part starts from the defaults: carrying the
|
|
131
186
|
last part's choices over fastened a shutter curtain level with a right edge nobody chose for it (2026-09-25).
|
|
132
187
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
188
|
+
/* The part the forms were loaded for. A panel made with a part already chosen loaded nothing, and said "editing the
|
|
189
|
+
fastening it has" over an empty target (2026-09-27, :3391, the doors). */
|
|
190
|
+
private loadedFor?: string
|
|
191
|
+
willUpdate(_changed: Map<string, unknown>) {
|
|
192
|
+
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
193
|
+
this.loadedFor = this.partId
|
|
135
194
|
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
136
195
|
const own = this.asset && this.partId ? fasteningOf(this.asset, this.partId) : null
|
|
137
196
|
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
@@ -142,8 +201,28 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
142
201
|
}
|
|
143
202
|
}
|
|
144
203
|
|
|
204
|
+
updated() {
|
|
205
|
+
/* Only the section that holds the fastening form speaks of it: the panel has one of these per section, and the
|
|
206
|
+
others' silence cleared what it showed (2026-09-27, :3391). */
|
|
207
|
+
if (this.section !== 'shape' && !this.told) return
|
|
208
|
+
const said = JSON.stringify(this.candidate ?? null)
|
|
209
|
+
if (said === this.told) return
|
|
210
|
+
this.told = said
|
|
211
|
+
this.dispatchEvent(new CustomEvent('preview-command', { detail: this.candidate ?? null, bubbles: true, composed: true }))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
disconnectedCallback() {
|
|
215
|
+
super.disconnectedCallback()
|
|
216
|
+
/* The panel left: what it showed goes with it. */
|
|
217
|
+
if (this.told && this.told !== 'null') this.dispatchEvent(new CustomEvent('preview-command', { detail: null, bubbles: true, composed: true }))
|
|
218
|
+
this.told = ''
|
|
219
|
+
}
|
|
220
|
+
|
|
145
221
|
render() {
|
|
146
222
|
const asset = this.asset
|
|
223
|
+
this.candidate = undefined
|
|
224
|
+
this.fastenCandidate = undefined
|
|
225
|
+
this.rowCandidate = undefined
|
|
147
226
|
if (!asset || !this.partId) return nothing
|
|
148
227
|
if (this.section === 'motion')
|
|
149
228
|
return html`<section><h3>${i18next.t('figure.label.motion')}${this.help.button('figure.text.motion-explained')}</h3>${this.help.hint('figure.text.motion-explained')}
|
|
@@ -151,18 +230,20 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
151
230
|
if (this.section === 'places')
|
|
152
231
|
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')}
|
|
153
232
|
${placeFacesOf(asset, this.partId).length ? this.renderPlaces(asset) : html`<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`
|
|
154
|
-
|
|
233
|
+
const shown = html`
|
|
155
234
|
<section>
|
|
156
|
-
<h3>${i18next.t('figure.label.
|
|
235
|
+
<h3>${i18next.t('figure.label.resize-intent')}</h3>
|
|
157
236
|
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
158
237
|
</section>
|
|
159
|
-
<section>
|
|
238
|
+
<section fastening ?spotlit=${this.spotlit} @focusin=${() => (this.working = 'fasten')}>
|
|
160
239
|
<h3>${i18next.t('figure.label.placement')}${this.help.button('figure.text.fastening-explained')}</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
161
240
|
${this.renderPositions(asset)}
|
|
162
241
|
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
163
242
|
</section>
|
|
164
243
|
${this.renderRow(asset)}
|
|
165
244
|
`
|
|
245
|
+
this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate
|
|
246
|
+
return shown
|
|
166
247
|
}
|
|
167
248
|
|
|
168
249
|
/** Where things are held on this part: a seat things rest on, or a grip things are held in. */
|
|
@@ -236,16 +317,95 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
236
317
|
}
|
|
237
318
|
|
|
238
319
|
private renderDimensions(asset: FigureAsset) {
|
|
320
|
+
/*
|
|
321
|
+
The kernel's plan for "follow the size and keep where the figure is placed" (ADR-0097), with the directions the
|
|
322
|
+
author ticked below -- a pending choice, drawn apart from the rows above, which only say what each direction does
|
|
323
|
+
now (designer 2026-09-27: the immediate and the pending are not mixed in one look).
|
|
324
|
+
*/
|
|
325
|
+
const editable = followableAxes(asset, this.partId)
|
|
326
|
+
const choice = resizeAxesOf(asset, this.partId, this.pendingAxes[this.partId])
|
|
327
|
+
const axes = choice.axes
|
|
328
|
+
const plan = planFigureResize(asset, { part: this.partId, axes })
|
|
329
|
+
const mode = plan.mode
|
|
330
|
+
const round = editable.length === 2
|
|
331
|
+
const word = (axis: Axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
|
|
332
|
+
const scope =
|
|
333
|
+
axes.length && axes.length === editable.length
|
|
334
|
+
? i18next.t(`figure.text.resize-scope-all-${editable.length}`)
|
|
335
|
+
: i18next.t('figure.text.resize-scope-only', { axes: axes.map(word).join(' · ') })
|
|
336
|
+
const ask = (gap?: 'keep' | 'plane') =>
|
|
337
|
+
this.dispatchEvent(new CustomEvent('resize-with-placement', { detail: { request: { part: this.partId, axes, ...(gap ? { gap } : {}) } }, bubbles: true, composed: true }))
|
|
338
|
+
const gapChoice = plan.refusal?.code === 'GAP_CHOICE' ? plan.refusal : undefined
|
|
339
|
+
/* Brought onto the plane, where it moves -- said on the button before it is pressed. */
|
|
340
|
+
const onto = gapChoice ? planFigureResize(asset, { part: this.partId, axes, gap: 'plane' }).moves[0] : undefined
|
|
341
|
+
const place = i18next.t(`figure.label.resize-place-${mode}`)
|
|
342
|
+
const tick = (axis: Axis, on: boolean) => {
|
|
343
|
+
const next = on ? [...new Set([...axes, axis])] : axes.filter(one => one !== axis)
|
|
344
|
+
this.pendingAxes = { ...this.pendingAxes, [this.partId]: next }
|
|
345
|
+
}
|
|
239
346
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
|
|
240
|
-
const round = followableAxes(asset, this.partId).length === 2
|
|
241
347
|
/* One step for the whole part (figure-model follow-size): the size is declared from the parts if there is none. */
|
|
242
|
-
const followsAll =
|
|
348
|
+
const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance')
|
|
243
349
|
const whole = followsAll
|
|
244
350
|
? nothing
|
|
245
351
|
: html`<button act follow-size ?disabled=${this.locked} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
246
352
|
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
247
353
|
</button>`
|
|
248
|
-
|
|
354
|
+
const held = plan.refusal?.code === 'HELD'
|
|
355
|
+
return html`<p hint>${i18next.t('figure.text.resize-intent')}</p>
|
|
356
|
+
${asset.sizing === 'uniform' ? html`<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
|
|
357
|
+
<div resize-now>${editable.map(axis => {
|
|
358
|
+
const now = followOf(asset, this.partId, axis)
|
|
359
|
+
const following = now.of === 'instance' && now.axis === axis
|
|
360
|
+
const custom = now.of !== 'own' && !following
|
|
361
|
+
const mm = measureNow(asset, this.partId, axis)
|
|
362
|
+
return html`<div row>
|
|
363
|
+
<b>${word(axis)}</b>
|
|
364
|
+
<span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
|
|
365
|
+
<span>${i18next.t(custom ? 'figure.label.resize-custom' : following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
|
|
366
|
+
${following
|
|
367
|
+
? html`<button link resize-unfollow=${axis} ?disabled=${this.locked}
|
|
368
|
+
@click=${() => {
|
|
369
|
+
/* Set back to fixed: the author's choice for this part from now on, not a blank to fill with "all". */
|
|
370
|
+
this.pendingAxes = { ...this.pendingAxes, [this.partId]: axes.filter(one => one !== axis) }
|
|
371
|
+
this.follow(asset, axis, 'own', now)
|
|
372
|
+
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
373
|
+
: nothing}
|
|
374
|
+
</div>`
|
|
375
|
+
})}</div>
|
|
376
|
+
${held
|
|
377
|
+
? this.renderHeld(asset)
|
|
378
|
+
: html`<fieldset resize-pending ?disabled=${this.locked || asset.sizing === 'uniform'}>
|
|
379
|
+
<legend>${i18next.t('figure.label.resize-pending')}</legend>
|
|
380
|
+
${editable.map(axis => {
|
|
381
|
+
const now = followOf(asset, this.partId, axis)
|
|
382
|
+
const following = now.of === 'instance' && now.axis === axis
|
|
383
|
+
const custom = now.of !== 'own' && !following
|
|
384
|
+
return html`<label>
|
|
385
|
+
<input type="checkbox" resize-tick=${axis} .checked=${axes.includes(axis)} ?disabled=${following || custom}
|
|
386
|
+
@change=${(e: Event) => tick(axis, (e.target as HTMLInputElement).checked)} />
|
|
387
|
+
${word(axis)}${following ? html` <small>${i18next.t('figure.text.resize-already-following')}</small>` : nothing}
|
|
388
|
+
</label>`
|
|
389
|
+
})}
|
|
390
|
+
<p hint resize-basis>${i18next.t(`figure.text.resize-basis-${choice.basis}`)}</p>
|
|
391
|
+
<p hint>${i18next.t('figure.text.placement-intent-' + mode)}</p>
|
|
392
|
+
${gapChoice
|
|
393
|
+
? 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>
|
|
394
|
+
<button act resize-keep-gap @click=${() => ask('keep')}>
|
|
395
|
+
${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)) })}
|
|
396
|
+
</button>
|
|
397
|
+
<button act resize-onto-plane @click=${() => ask('plane')}>
|
|
398
|
+
${i18next.t('figure.button.resize-onto-plane-' + mode, { scope })}
|
|
399
|
+
${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}
|
|
400
|
+
</button>`
|
|
401
|
+
: html`<button act resize-with-placement ?disabled=${choice.basis === 'none' || plan.refusal?.code === 'UNIFORM_POLICY'} @click=${() => ask()}>
|
|
402
|
+
${i18next.t('figure.button.resize-scoped-' + mode, { scope })}
|
|
403
|
+
</button>`}
|
|
404
|
+
${!plan.refusal && plan.replaces.length
|
|
405
|
+
? 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>`
|
|
406
|
+
: nothing}
|
|
407
|
+
</fieldset>`}
|
|
408
|
+
<details><summary>${i18next.t('figure.label.resize-advanced')}</summary>${whole}${followableAxes(asset, this.partId).map(axis => {
|
|
249
409
|
const now = followOf(asset, this.partId, axis)
|
|
250
410
|
const value = choiceOf(now)
|
|
251
411
|
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
@@ -285,7 +445,68 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
285
445
|
</label>`
|
|
286
446
|
: nothing}
|
|
287
447
|
</div>`
|
|
288
|
-
})}
|
|
448
|
+
})}</details>`
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* "Open attachment settings": the fastening this part has now -- its face, what it is on, the gap -- in the form below,
|
|
453
|
+
* brought into view with the focus on it. The form is loaded again first: the part was fastened by "follow size,
|
|
454
|
+
* attached" while it was selected, so the form still held what it had before (designer 2026-09-27: it opened the
|
|
455
|
+
* holding places instead).
|
|
456
|
+
*/
|
|
457
|
+
/** The fastening's details open, focus in them: where the gap and the place across the face are changed. */
|
|
458
|
+
private async openDetail() {
|
|
459
|
+
const detail = this.renderRoot.querySelector<HTMLDetailsElement>('details[fasten-detail]')
|
|
460
|
+
if (!detail) return
|
|
461
|
+
detail.open = true
|
|
462
|
+
this.working = 'fasten'
|
|
463
|
+
await this.updateComplete
|
|
464
|
+
detail.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
465
|
+
detail.querySelector<HTMLElement>('select, input')?.focus({ preventScroll: true })
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
|
|
469
|
+
async showFastening() {
|
|
470
|
+
if (this.asset) await this.openFastening(this.asset)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async openFastening(asset: FigureAsset) {
|
|
474
|
+
const own = fasteningOf(asset, this.partId)
|
|
475
|
+
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
476
|
+
this.spotlit = true
|
|
477
|
+
await this.updateComplete
|
|
478
|
+
const section = this.renderRoot.querySelector<HTMLElement>('section[fastening]')
|
|
479
|
+
section?.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
480
|
+
/* Into the form itself, not the section's help button. */
|
|
481
|
+
;(section?.querySelector<HTMLElement>('select, input') ?? section?.querySelector<HTMLElement>('button'))?.focus({ preventScroll: true })
|
|
482
|
+
setTimeout(() => (this.spotlit = false), 1600)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* What holds the part, said as its state -- 「아랫면을 바닥에 유지 중」 -- with the ways to change it: open the fastening,
|
|
487
|
+
* or take it off. Right after "follow size, attached" this is the result, not a refusal (designer 2026-09-27).
|
|
488
|
+
*/
|
|
489
|
+
private renderHeld(asset: FigureAsset) {
|
|
490
|
+
const joint = (asset.joints ?? []).find(j => j.body1.part === this.partId)
|
|
491
|
+
const face = joint ? i18next.t(`figure.label.face-${joint.body1.face}`) : ''
|
|
492
|
+
const gap = typeof joint?.origin?.gap === 'number' ? joint.origin.gap : 0
|
|
493
|
+
const where = !joint
|
|
494
|
+
? i18next.t('figure.text.held-by-rule')
|
|
495
|
+
: 'plane' in joint.body0
|
|
496
|
+
? i18next.t('figure.text.held-on-plane', { face, place: i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`), gap: String(gap) })
|
|
497
|
+
: i18next.t('figure.text.held-on-part', { face, other: joint.body0.part, otherFace: i18next.t(`figure.label.face-${joint.body0.face}`) })
|
|
498
|
+
return html`<div resize-held>
|
|
499
|
+
<p><md-icon>link</md-icon> ${where}</p>
|
|
500
|
+
<button act open-fastening @click=${() => this.openFastening(asset)}>
|
|
501
|
+
${i18next.t('figure.button.open-fastening')}
|
|
502
|
+
</button>
|
|
503
|
+
${joint
|
|
504
|
+
? html`<button act detach-held ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
505
|
+
${i18next.t('figure.button.detach-held')}
|
|
506
|
+
</button>
|
|
507
|
+
${this.renderDetachWords()}`
|
|
508
|
+
: nothing}
|
|
509
|
+
</div>`
|
|
289
510
|
}
|
|
290
511
|
|
|
291
512
|
/** The same source, the same share, a new difference: a leaf the opening less its gaps (link-dimension's `plus`). */
|
|
@@ -347,14 +568,16 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
347
568
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
348
569
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
349
570
|
const aim = f.toPlane ? ('plane' as const) : f.target ? { part: f.target, face: targetFace } : undefined
|
|
350
|
-
this.fasten = { ...f, ...(aim ? placeKeptOf(asset, this.partId, aim, !!f.twist) : { align: {}, alignMm: {} }) }
|
|
571
|
+
this.fasten = { ...f, ...(aim ? placeKeptOf(asset, this.partId, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) }
|
|
351
572
|
}
|
|
352
573
|
|
|
353
574
|
private renderFastening(asset: FigureAsset) {
|
|
354
575
|
const held = heldBy(asset, this.partId)
|
|
355
576
|
const f = this.fasten
|
|
356
577
|
/* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */
|
|
357
|
-
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one)
|
|
578
|
+
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one))
|
|
579
|
+
/* Shown, not dropped, and not choosable, with the reason (ADR-0097 §5.5): fastening it would close a loop. */
|
|
580
|
+
const loops = (one: string) => hangsFrom(asset, one, this.partId)
|
|
358
581
|
/* The faces are the ones each shape declares: a part is fastened by one of its outer faces, to any face of the target. */
|
|
359
582
|
/* On the mounting plane only the faces it takes: a bottom or a top on the floor, a back on a wall. */
|
|
360
583
|
const plane = mountingPlaneOf(asset)
|
|
@@ -367,15 +590,66 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
367
590
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
368
591
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
369
592
|
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: targetFace }
|
|
370
|
-
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist) : undefined
|
|
593
|
+
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist, f.alignInput) : undefined
|
|
594
|
+
const action =
|
|
595
|
+
f.toPlane || f.target
|
|
596
|
+
? ({
|
|
597
|
+
kind: 'attach',
|
|
598
|
+
part: this.partId,
|
|
599
|
+
face: myFace,
|
|
600
|
+
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
601
|
+
...(f.toPlane ? {} : { facing: f.facing }),
|
|
602
|
+
...(f.gap ? { gap: f.gap } : {}),
|
|
603
|
+
twist: f.twist,
|
|
604
|
+
...(align ? { align } : {}),
|
|
605
|
+
replace: !!held
|
|
606
|
+
} as FigureAuthoringAction)
|
|
607
|
+
: undefined
|
|
608
|
+
if (action) this.fastenCandidate = { action, ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } }) }
|
|
609
|
+
/* What the details hold that is not their default, and where on the face it sits: said while they are folded. */
|
|
610
|
+
const detail = [
|
|
611
|
+
...(f.facing === 'flush' && !f.toPlane ? [i18next.t('figure.label.facing-flush')] : []),
|
|
612
|
+
...Object.entries(align ?? {}).map(([axis, one]) =>
|
|
613
|
+
one === 'min' || one === 'max'
|
|
614
|
+
? i18next.t(`figure.text.fasten-summary-edge-${one}`, { axis: axis.toUpperCase() })
|
|
615
|
+
: typeof one.mm === 'number'
|
|
616
|
+
? i18next.t('figure.text.fasten-summary-off-centre', { axis: axis.toUpperCase(), mm: String(Math.round(one.mm * 10) / 10) })
|
|
617
|
+
: i18next.t('figure.text.fasten-summary-follows', { axis: axis.toUpperCase() })
|
|
618
|
+
),
|
|
619
|
+
...(f.gap ? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })] : []),
|
|
620
|
+
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
621
|
+
]
|
|
622
|
+
/*
|
|
623
|
+
Editing the fastening it has, or fastening it anew: said, with the relation named, so the form is not taken for a
|
|
624
|
+
blank new one while it holds the current (designer 2026-09-27). Applied unchanged, the same fastening is written
|
|
625
|
+
again, fields and all (resize-with-placement.test).
|
|
626
|
+
*/
|
|
627
|
+
const current = asset.joints?.find(j => j.body1.part === this.partId)
|
|
628
|
+
const editing = current
|
|
629
|
+
? i18next.t('figure.text.fasten-editing', {
|
|
630
|
+
face: i18next.t(`figure.label.face-${current.body1.face}`),
|
|
631
|
+
target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
|
|
632
|
+
})
|
|
633
|
+
: i18next.t('figure.text.fasten-new')
|
|
371
634
|
return html`
|
|
635
|
+
<p fasten-mode ?editing=${!!current}>${editing}</p>
|
|
636
|
+
${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
637
|
+
${this.overhangWords.length
|
|
638
|
+
? html`<div overhang role="status">
|
|
639
|
+
${this.overhangWords.map(line => html`<p>${line}</p>`)}
|
|
640
|
+
${current
|
|
641
|
+
? html`<button act @click=${() => this.openDetail()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>`
|
|
642
|
+
: nothing}
|
|
643
|
+
</div>`
|
|
644
|
+
: nothing}
|
|
372
645
|
${held
|
|
373
646
|
? html`<div pair>
|
|
374
647
|
<span>${i18next.t('figure.label.fastened-to')}: <b bound>${'plane' in held ? i18next.t('figure.label.mounting-plane') : held.part}</b></span>
|
|
375
648
|
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
376
649
|
<md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
|
|
377
650
|
</button>
|
|
378
|
-
</div
|
|
651
|
+
</div>
|
|
652
|
+
${this.renderDetachWords()}`
|
|
379
653
|
: nothing}
|
|
380
654
|
<div form>
|
|
381
655
|
<span>${i18next.t('figure.label.my-face')}</span>
|
|
@@ -393,46 +667,47 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
393
667
|
<span>${i18next.t('figure.label.target-part')}</span>
|
|
394
668
|
<select @change=${(e: Event) => this.aimAt(asset, { ...f, target: (e.target as HTMLSelectElement).value })}>
|
|
395
669
|
<option value="" ?selected=${!f.target}>—</option>
|
|
396
|
-
${targets.map(one =>
|
|
670
|
+
${targets.map(one =>
|
|
671
|
+
loops(one)
|
|
672
|
+
? html`<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
673
|
+
: html`<option value=${one} ?selected=${one === f.target}>${one}</option>`
|
|
674
|
+
)}
|
|
397
675
|
</select>
|
|
398
676
|
<span>${i18next.t('figure.label.target-face')}</span>
|
|
399
677
|
<select @change=${(e: Event) => this.aimAt(asset, { ...f, targetFace: (e.target as HTMLSelectElement).value as FigureFace })}>
|
|
400
678
|
${theirFaces.map(face => html`<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
401
679
|
</select>
|
|
402
|
-
<span>${i18next.t('figure.label.facing-meet')}</span>
|
|
403
|
-
<select @change=${(e: Event) => (this.fasten = { ...f, facing: (e.target as HTMLSelectElement).value as 'meet' | 'flush' })}>
|
|
404
|
-
<option value="meet" ?selected=${f.facing === 'meet'}>${i18next.t('figure.label.facing-meet')}</option>
|
|
405
|
-
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
406
|
-
</select>
|
|
407
680
|
</div>`}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
>
|
|
431
|
-
<md-icon>link</md-icon>${i18next.t('figure.button.fasten')}
|
|
681
|
+
<details fasten-detail>
|
|
682
|
+
<summary>${i18next.t('figure.label.fasten-detail')} · <span fasten-summary>${detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')}</span></summary>
|
|
683
|
+
${f.toPlane
|
|
684
|
+
? nothing
|
|
685
|
+
: html`<div form>
|
|
686
|
+
<span>${i18next.t('figure.label.facing')}</span>
|
|
687
|
+
<select @change=${(e: Event) => (this.fasten = { ...f, facing: (e.target as HTMLSelectElement).value as 'meet' | 'flush' })}>
|
|
688
|
+
<option value="meet" ?selected=${f.facing === 'meet'}>${i18next.t('figure.label.facing-meet')}</option>
|
|
689
|
+
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
690
|
+
</select>
|
|
691
|
+
</div>`}
|
|
692
|
+
${this.renderAlignment(f.toPlane || f.target ? alignAxesOf(asset, this.partId, aim, !!f.twist) : [])}
|
|
693
|
+
<div form>
|
|
694
|
+
<span>${i18next.t('figure.label.gap')}</span>
|
|
695
|
+
<input type="number" step="any" .value=${String(f.gap)} @change=${(e: Event) => (this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
696
|
+
<span>${i18next.t('figure.label.twist')}</span>
|
|
697
|
+
<input type="number" step="any" .value=${String(f.twist)} @change=${(e: Event) => (this.fasten = { ...f, twist: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
698
|
+
</div>
|
|
699
|
+
</details>
|
|
700
|
+
${action && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
|
|
701
|
+
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
702
|
+
<md-icon>link</md-icon>${i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')}
|
|
432
703
|
</button>
|
|
433
704
|
`
|
|
434
705
|
}
|
|
435
706
|
|
|
707
|
+
private renderDetachWords() {
|
|
708
|
+
return this.detachWords.length ? html`<ul detach-words>${this.detachWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
|
|
709
|
+
}
|
|
710
|
+
|
|
436
711
|
/**
|
|
437
712
|
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
438
713
|
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
@@ -451,6 +726,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
451
726
|
? html`<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
452
727
|
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>`
|
|
453
728
|
: nothing}
|
|
729
|
+
${f.alignInput?.[axis]
|
|
730
|
+
? html`<option value="input" ?selected=${how === 'input'}>${i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis]!, key => i18next.t(key)) })}</option>`
|
|
731
|
+
: nothing}
|
|
454
732
|
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
455
733
|
${this.asset && hasInstanceSize(this.asset, axis)
|
|
456
734
|
? html`<option value="share" ?selected=${how === 'share'}>${i18next.t('figure.label.align-share')}</option>`
|
|
@@ -468,37 +746,59 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
468
746
|
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
469
747
|
* (the editor's) shows it and takes it away.
|
|
470
748
|
*/
|
|
749
|
+
private renderPreviewWords() {
|
|
750
|
+
return this.previewWords.length ? html`<ul preview-words role="status">${this.previewWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* A row (ADR-0097 §5.5): said as what it makes, "arranged in a row". Where it cannot be made here, why, rather than
|
|
755
|
+
* nothing. Filling the length, the centre spacing is the author's; a count of their own is spread evenly over the
|
|
756
|
+
* length -- figure-model draws it so, and the form does not ask for a spacing it would not use.
|
|
757
|
+
*/
|
|
471
758
|
private renderRow(asset: FigureAsset) {
|
|
759
|
+
if (rowOf(asset, this.partId)) return nothing
|
|
760
|
+
const blocked = rowBlockOf(asset, this.partId)
|
|
472
761
|
const axes = rowAxesOf(asset, this.partId)
|
|
473
|
-
if (!axes.length) return nothing
|
|
474
762
|
const r = this.row
|
|
763
|
+
if (blocked || !axes.length)
|
|
764
|
+
return html`<section row>
|
|
765
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
766
|
+
<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>
|
|
767
|
+
${blocked === 'unfastened'
|
|
768
|
+
? html`<button act @click=${() => this.openFastening(asset)}><md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}</button>`
|
|
769
|
+
: nothing}
|
|
770
|
+
</section>`
|
|
475
771
|
const axis = axes.includes(r.axis as Axis) ? (r.axis as Axis) : axes[0]!
|
|
476
|
-
const ok = r.pitch > 0
|
|
477
|
-
|
|
772
|
+
const ok = r.fills ? r.pitch > 0 : Number.isSafeInteger(r.count) && r.count >= 1
|
|
773
|
+
const action = ok
|
|
774
|
+
? ({ kind: 'make-repeat', part: this.partId, axis, pitch: r.fills ? r.pitch : Math.max(r.pitch, 1), ...(r.fills ? {} : { count: r.count }) } as FigureAuthoringAction)
|
|
775
|
+
: undefined
|
|
776
|
+
this.rowCandidate = action
|
|
777
|
+
const set = (next: typeof r) => ((this.row = next), (this.working = 'row'))
|
|
778
|
+
return html`<section row @focusin=${() => (this.working = 'row')}>
|
|
478
779
|
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
479
780
|
<div form>
|
|
480
781
|
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
481
|
-
<select @change=${(e: Event) => (
|
|
782
|
+
<select @change=${(e: Event) => set({ ...r, axis: (e.target as HTMLSelectElement).value as Axis })}>
|
|
482
783
|
${axes.map(a => html`<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}
|
|
483
784
|
</select>
|
|
484
|
-
<span>${i18next.t('figure.label.repeat-pitch')} (mm)</span>
|
|
485
|
-
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e: Event) => (this.row = { ...r, pitch: Number((e.target as HTMLInputElement).value) })} />
|
|
486
785
|
</div>
|
|
487
|
-
<label
|
|
488
|
-
<input type="
|
|
489
|
-
|
|
490
|
-
</
|
|
786
|
+
<div row-basis role="radiogroup" aria-label=${i18next.t('figure.label.row-basis')}>
|
|
787
|
+
<label><input type="radio" name="row-basis" .checked=${r.fills} @change=${() => set({ ...r, fills: true })} />${i18next.t('figure.label.repeat-fills')}</label>
|
|
788
|
+
<label><input type="radio" name="row-basis" .checked=${!r.fills} @change=${() => set({ ...r, fills: false })} />${i18next.t('figure.label.row-by-count')}</label>
|
|
789
|
+
</div>
|
|
491
790
|
${r.fills
|
|
492
|
-
?
|
|
791
|
+
? html`<div form>
|
|
792
|
+
<span>${i18next.t('figure.label.repeat-pitch')} · mm</span>
|
|
793
|
+
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e: Event) => set({ ...r, pitch: Number((e.target as HTMLInputElement).value) })} />
|
|
794
|
+
</div>`
|
|
493
795
|
: html`<div form>
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
@click=${() => this.command({ kind: 'make-repeat', part: this.partId, axis, pitch: r.pitch, ...(r.fills ? {} : { count: r.count }) } as FigureAuthoringAction)}
|
|
501
|
-
>
|
|
796
|
+
<span>${i18next.t('figure.label.repeat-count')}</span>
|
|
797
|
+
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e: Event) => set({ ...r, count: Number((e.target as HTMLInputElement).value) })} />
|
|
798
|
+
</div>
|
|
799
|
+
<p hint>${i18next.t('figure.text.row-count-spreads')}</p>`}
|
|
800
|
+
${action && this.working === 'row' ? this.renderPreviewWords() : nothing}
|
|
801
|
+
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
502
802
|
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
503
803
|
</button>
|
|
504
804
|
</section>`
|
|
@@ -551,7 +851,21 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
551
851
|
const now = motionOf(asset, this.partId)
|
|
552
852
|
const joint = asset.joints?.find(j => j.body1.part === this.partId)
|
|
553
853
|
if (now && !(this.editingMotion && joint)) {
|
|
554
|
-
|
|
854
|
+
/* Which part moves, against what, which way, how far -- by name (ADR-0097 §5.5). */
|
|
855
|
+
const against = joint ? ('plane' in joint.body0 ? i18next.t('figure.label.mounting-plane') : joint.body0.part) : ''
|
|
856
|
+
const ends = this.motionEnds
|
|
857
|
+
return html`<p motion-summary>
|
|
858
|
+
${i18next.t('figure.text.motion-summary', {
|
|
859
|
+
part: this.partId,
|
|
860
|
+
against,
|
|
861
|
+
kind: i18next.t('figure.label.motion-' + now.kind),
|
|
862
|
+
axis: String(now.axis).toUpperCase()
|
|
863
|
+
})}
|
|
864
|
+
</p>
|
|
865
|
+
${ends
|
|
866
|
+
? html`<p hint motion-ends>${i18next.t('figure.text.motion-ends-shown', { min: String(ends.min), max: String(ends.max), unit: ends.unit })}</p>`
|
|
867
|
+
: nothing}
|
|
868
|
+
<div pair>
|
|
555
869
|
<span>
|
|
556
870
|
${i18next.t('figure.label.motion-' + now.kind)} · ${String(now.axis).toUpperCase()}
|
|
557
871
|
${now.travel
|
|
@@ -566,7 +880,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
566
880
|
<button
|
|
567
881
|
act
|
|
568
882
|
?disabled=${this.locked || !joint}
|
|
569
|
-
@click=${() => joint && this.command({ kind: 'set-joint', joint:
|
|
883
|
+
@click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}
|
|
570
884
|
>
|
|
571
885
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
572
886
|
</button>
|
|
@@ -584,7 +898,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
584
898
|
</div>`
|
|
585
899
|
}
|
|
586
900
|
/* A joint needs a parent (URDF): a part fastened to nothing is fastened first. */
|
|
587
|
-
if (!joint)
|
|
901
|
+
if (!joint)
|
|
902
|
+
return html`<p hint>${i18next.t('figure.text.fasten-before-moving')}</p>
|
|
903
|
+
<button act go-fasten @click=${() => this.dispatchEvent(new CustomEvent('go-fasten', { bubbles: true, composed: true }))}>
|
|
904
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}
|
|
905
|
+
</button>`
|
|
588
906
|
const m = this.motion
|
|
589
907
|
const unit = m.kind === 'slide' ? 'mm' : 'deg'
|
|
590
908
|
/* A follower follows a joint that moves the same way; a choice the kind no longer allows is dropped. */
|
|
@@ -667,4 +985,3 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
667
985
|
this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))
|
|
668
986
|
}
|
|
669
987
|
}
|
|
670
|
-
|