@things-factory/figure-ui 10.1.81 → 10.1.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/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 +800 -53
- 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 +357 -94
- 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/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 +107 -1
- package/dist-client/modeller/editor.js +823 -49
- 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-capability-panel.d.ts +1 -1
- package/dist-client/modeller/figure-clips.d.ts +1 -1
- package/dist-client/modeller/figure-controls.d.ts +1 -1
- package/dist-client/modeller/figure-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 +52 -2
- package/dist-client/modeller/figure-links.js +366 -97
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +1 -1
- package/dist-client/modeller/figure-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.d.ts +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/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 +9 -5
- 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 +226 -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 +229 -11
- package/translations/ja.json +229 -11
- package/translations/ko.json +230 -12
- package/translations/ms.json +228 -10
- package/translations/zh.json +229 -11
|
@@ -38,7 +38,12 @@ import {
|
|
|
38
38
|
placeIdFor,
|
|
39
39
|
placesOn,
|
|
40
40
|
rowAxesOf,
|
|
41
|
+
rowBlockOf,
|
|
42
|
+
fixedJointOf,
|
|
43
|
+
rowOf,
|
|
41
44
|
positionsWorkedOut,
|
|
45
|
+
fieldWordsOf,
|
|
46
|
+
measureNow,
|
|
42
47
|
type Alignment,
|
|
43
48
|
type Axis,
|
|
44
49
|
type Follow,
|
|
@@ -58,7 +63,8 @@ const freshFasten = () => ({
|
|
|
58
63
|
gap: 0,
|
|
59
64
|
twist: 0,
|
|
60
65
|
align: {} as Partial<Record<Axis, Alignment>>,
|
|
61
|
-
alignMm: {} as Partial<Record<Axis, number
|
|
66
|
+
alignMm: {} as Partial<Record<Axis, number>>,
|
|
67
|
+
alignInput: {} as Partial<Record<Axis, string>>
|
|
62
68
|
})
|
|
63
69
|
const freshMotion = (): MotionForm => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 })
|
|
64
70
|
const freshRow = () => ({ axis: '' as Axis | '', pitch: 200, fills: true, count: 5 })
|
|
@@ -97,6 +103,29 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
97
103
|
b[bound] {
|
|
98
104
|
font-weight: 600;
|
|
99
105
|
}
|
|
106
|
+
/* What each direction does now: read-only rows. */
|
|
107
|
+
div[resize-now] { display: grid; gap: 4px; margin: 12px 0; }
|
|
108
|
+
div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
|
|
109
|
+
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
110
|
+
button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
|
|
111
|
+
/* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
|
|
112
|
+
details[resize-choice] { margin: 12px 0; padding: 0; border: 0; }
|
|
113
|
+
details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
|
|
114
|
+
div[resize-held] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
|
|
115
|
+
div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
|
|
116
|
+
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
117
|
+
p[fasten-mode][editing] { color: var(--md-sys-color-primary, #477b9e); }
|
|
118
|
+
section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
|
|
119
|
+
details { margin-top: 14px; }
|
|
120
|
+
summary { cursor: pointer; padding: 8px 0; }
|
|
121
|
+
/* What the command would do, said beside the result drawn on the canvas. */
|
|
122
|
+
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; }
|
|
123
|
+
div[row-basis] { display: grid; gap: 4px; margin: 8px 0; }
|
|
124
|
+
div[row-basis] label { display: flex; align-items: center; gap: 6px; }
|
|
125
|
+
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); }
|
|
126
|
+
div[overhang] p { margin: 0; }
|
|
127
|
+
ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; font-size: 0.8rem; opacity: 0.85; }
|
|
128
|
+
span[fasten-summary] { opacity: 0.8; }
|
|
100
129
|
/* The difference a following measure keeps: under its choice, on the same line as its words. */
|
|
101
130
|
label[inline-plus] {
|
|
102
131
|
display: flex;
|
|
@@ -118,7 +147,31 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
118
147
|
@property({ type: Boolean }) locked = false
|
|
119
148
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
120
149
|
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
150
|
+
/** What the fastening command would do, in words (the editor works it out: `command-preview.ts`). */
|
|
151
|
+
@property({ attribute: false }) previewWords: string[] = []
|
|
152
|
+
/** Where the selected part's motion takes it, at both ends of its range (the editor draws them on the canvas). */
|
|
153
|
+
@property({ attribute: false }) motionEnds: { control: string; min: number; max: number; unit: string } | null = null
|
|
154
|
+
/** Past the edge of the face it is fastened to, or parts fastened to it past theirs (ADR-0097 §5.5, Q3). */
|
|
155
|
+
@property({ attribute: false }) overhangWords: string[] = []
|
|
156
|
+
/** Wider than the face it is fastened to: how far past each edge, and whether off to one side -- a fact, not a warning. */
|
|
157
|
+
@property({ attribute: false }) overhangNote: string[] = []
|
|
158
|
+
/** What taking this part off would do, said beside the buttons that do it (worked out on a copy by the editor). */
|
|
159
|
+
@property({ attribute: false }) detachWords: string[] = []
|
|
160
|
+
/*
|
|
161
|
+
The fastening command the form describes now, and the face it aims at: told to the editor whenever it changes, so
|
|
162
|
+
the result is seen before it is done (ADR-0097 §5.5). Nothing is told while no target is chosen.
|
|
163
|
+
*/
|
|
164
|
+
private candidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
165
|
+
private told = ''
|
|
166
|
+
/* Two forms describe a command here: the fastening and the row. The one being worked on is the one seen. */
|
|
167
|
+
@state() private working: 'fasten' | 'row' = 'fasten'
|
|
168
|
+
private fastenCandidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
169
|
+
private rowCandidate?: FigureAuthoringAction
|
|
121
170
|
|
|
171
|
+
/** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
|
|
172
|
+
@state() private choosingFastening = false
|
|
173
|
+
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
174
|
+
@state() private spotlit = false
|
|
122
175
|
@state() private fasten = freshFasten()
|
|
123
176
|
@state() private motion: MotionForm = freshMotion()
|
|
124
177
|
@state() private place = freshPlace()
|
|
@@ -130,8 +183,12 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
130
183
|
The forms describe the next command for the selected part, so another part starts from the defaults: carrying the
|
|
131
184
|
last part's choices over fastened a shutter curtain level with a right edge nobody chose for it (2026-09-25).
|
|
132
185
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
186
|
+
/* The part the forms were loaded for. A panel made with a part already chosen loaded nothing, and said "editing the
|
|
187
|
+
fastening it has" over an empty target (2026-09-27, :3391, the doors). */
|
|
188
|
+
private loadedFor?: string
|
|
189
|
+
willUpdate(_changed: Map<string, unknown>) {
|
|
190
|
+
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
191
|
+
this.loadedFor = this.partId
|
|
135
192
|
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
136
193
|
const own = this.asset && this.partId ? fasteningOf(this.asset, this.partId) : null
|
|
137
194
|
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
@@ -139,11 +196,32 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
139
196
|
this.place = freshPlace()
|
|
140
197
|
this.row = freshRow()
|
|
141
198
|
this.editingMotion = false
|
|
199
|
+
this.choosingFastening = false
|
|
142
200
|
}
|
|
143
201
|
}
|
|
144
202
|
|
|
203
|
+
updated() {
|
|
204
|
+
/* Only the section that holds the fastening form speaks of it: the panel has one of these per section, and the
|
|
205
|
+
others' silence cleared what it showed (2026-09-27, :3391). */
|
|
206
|
+
if (this.section !== 'shape' && !this.told) return
|
|
207
|
+
const said = JSON.stringify(this.candidate ?? null)
|
|
208
|
+
if (said === this.told) return
|
|
209
|
+
this.told = said
|
|
210
|
+
this.dispatchEvent(new CustomEvent('preview-command', { detail: this.candidate ?? null, bubbles: true, composed: true }))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
disconnectedCallback() {
|
|
214
|
+
super.disconnectedCallback()
|
|
215
|
+
/* The panel left: what it showed goes with it. */
|
|
216
|
+
if (this.told && this.told !== 'null') this.dispatchEvent(new CustomEvent('preview-command', { detail: null, bubbles: true, composed: true }))
|
|
217
|
+
this.told = ''
|
|
218
|
+
}
|
|
219
|
+
|
|
145
220
|
render() {
|
|
146
221
|
const asset = this.asset
|
|
222
|
+
this.candidate = undefined
|
|
223
|
+
this.fastenCandidate = undefined
|
|
224
|
+
this.rowCandidate = undefined
|
|
147
225
|
if (!asset || !this.partId) return nothing
|
|
148
226
|
if (this.section === 'motion')
|
|
149
227
|
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 +229,20 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
151
229
|
if (this.section === 'places')
|
|
152
230
|
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
231
|
${placeFacesOf(asset, this.partId).length ? this.renderPlaces(asset) : html`<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`
|
|
154
|
-
|
|
232
|
+
const shown = html`
|
|
155
233
|
<section>
|
|
156
|
-
<h3>${i18next.t('figure.label.
|
|
234
|
+
<h3>${i18next.t('figure.label.resize-intent')}</h3>
|
|
157
235
|
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
158
236
|
</section>
|
|
159
|
-
<section>
|
|
237
|
+
<section fastening ?spotlit=${this.spotlit} @focusin=${() => (this.working = 'fasten')}>
|
|
160
238
|
<h3>${i18next.t('figure.label.placement')}${this.help.button('figure.text.fastening-explained')}</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
161
239
|
${this.renderPositions(asset)}
|
|
162
240
|
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
163
241
|
</section>
|
|
164
242
|
${this.renderRow(asset)}
|
|
165
243
|
`
|
|
244
|
+
this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate
|
|
245
|
+
return shown
|
|
166
246
|
}
|
|
167
247
|
|
|
168
248
|
/** Where things are held on this part: a seat things rest on, or a grip things are held in. */
|
|
@@ -236,16 +316,36 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
236
316
|
}
|
|
237
317
|
|
|
238
318
|
private renderDimensions(asset: FigureAsset) {
|
|
319
|
+
const editable = followableAxes(asset, this.partId)
|
|
320
|
+
const round = editable.length === 2
|
|
321
|
+
const word = (axis: Axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`)
|
|
239
322
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
323
|
+
const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance')
|
|
324
|
+
return html`<p hint>${i18next.t('figure.text.resize-intent')}</p>
|
|
325
|
+
${asset.sizing === 'uniform' ? html`<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
|
|
326
|
+
<div resize-now>${editable.map(axis => {
|
|
327
|
+
const now = followOf(asset, this.partId, axis)
|
|
328
|
+
const following = now.of === 'instance' && now.axis === axis
|
|
329
|
+
const custom = now.of !== 'own' && !following
|
|
330
|
+
const mm = measureNow(asset, this.partId, axis)
|
|
331
|
+
return html`<div row>
|
|
332
|
+
<b>${word(axis)}</b>
|
|
333
|
+
<span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
|
|
334
|
+
<span>${i18next.t(custom ? 'figure.label.resize-custom' : following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
|
|
335
|
+
${following
|
|
336
|
+
? html`<button link resize-unfollow=${axis} ?disabled=${this.locked}
|
|
337
|
+
@click=${() => {
|
|
338
|
+
this.follow(asset, axis, 'own', now)
|
|
339
|
+
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
340
|
+
: nothing}
|
|
341
|
+
</div>`
|
|
342
|
+
})}</div>
|
|
343
|
+
${followsAll
|
|
344
|
+
? html`<p hint>${i18next.t('figure.text.resize-all-following')}</p>`
|
|
345
|
+
: html`<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
346
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
347
|
+
</button>`}
|
|
348
|
+
<details resize-choice><summary>${i18next.t('figure.label.resize-advanced')}</summary><div>${followableAxes(asset, this.partId).map(axis => {
|
|
249
349
|
const now = followOf(asset, this.partId, axis)
|
|
250
350
|
const value = choiceOf(now)
|
|
251
351
|
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
@@ -285,7 +385,68 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
285
385
|
</label>`
|
|
286
386
|
: nothing}
|
|
287
387
|
</div>`
|
|
288
|
-
})}
|
|
388
|
+
})}</div></details>`
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* "Open attachment settings": the fastening this part has now -- its face, what it is on, the gap -- in the form below,
|
|
393
|
+
* brought into view with the focus on it. The form is loaded again first: the part was fastened by "follow size,
|
|
394
|
+
* attached" while it was selected, so the form still held what it had before (designer 2026-09-27: it opened the
|
|
395
|
+
* holding places instead).
|
|
396
|
+
*/
|
|
397
|
+
/** The fastening's details open, focus in them: where the gap and the place across the face are changed. */
|
|
398
|
+
private async openDetail() {
|
|
399
|
+
const detail = this.renderRoot.querySelector<HTMLDetailsElement>('details[fasten-detail]')
|
|
400
|
+
if (!detail) return
|
|
401
|
+
detail.open = true
|
|
402
|
+
this.working = 'fasten'
|
|
403
|
+
await this.updateComplete
|
|
404
|
+
detail.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
405
|
+
detail.querySelector<HTMLElement>('select, input')?.focus({ preventScroll: true })
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
|
|
409
|
+
async showFastening() {
|
|
410
|
+
if (this.asset) await this.openFastening(this.asset)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async openFastening(asset: FigureAsset) {
|
|
414
|
+
const own = fasteningOf(asset, this.partId)
|
|
415
|
+
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
416
|
+
this.spotlit = true
|
|
417
|
+
await this.updateComplete
|
|
418
|
+
const section = this.renderRoot.querySelector<HTMLElement>('section[fastening]')
|
|
419
|
+
section?.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
420
|
+
/* Into the form itself, not the section's help button. */
|
|
421
|
+
;(section?.querySelector<HTMLElement>('select, input') ?? section?.querySelector<HTMLElement>('button'))?.focus({ preventScroll: true })
|
|
422
|
+
setTimeout(() => (this.spotlit = false), 1600)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* What holds the part, said as its state -- 「아랫면을 바닥에 유지 중」 -- with the ways to change it: open the fastening,
|
|
427
|
+
* or take it off. Right after "follow size, attached" this is the result, not a refusal (designer 2026-09-27).
|
|
428
|
+
*/
|
|
429
|
+
private renderHeld(asset: FigureAsset) {
|
|
430
|
+
const joint = (asset.joints ?? []).find(j => j.body1.part === this.partId)
|
|
431
|
+
const face = joint ? i18next.t(`figure.label.face-${joint.body1.face}`) : ''
|
|
432
|
+
const gap = typeof joint?.origin?.gap === 'number' ? joint.origin.gap : 0
|
|
433
|
+
const where = !joint
|
|
434
|
+
? i18next.t('figure.text.held-by-rule')
|
|
435
|
+
: 'plane' in joint.body0
|
|
436
|
+
? i18next.t('figure.text.held-on-plane', { face, place: i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`), gap: String(gap) })
|
|
437
|
+
: i18next.t('figure.text.held-on-part', { face, other: joint.body0.part, otherFace: i18next.t(`figure.label.face-${joint.body0.face}`) })
|
|
438
|
+
return html`<div resize-held>
|
|
439
|
+
<p><md-icon>link</md-icon> ${where}</p>
|
|
440
|
+
<button act open-fastening @click=${() => this.openFastening(asset)}>
|
|
441
|
+
${i18next.t('figure.button.open-fastening')}
|
|
442
|
+
</button>
|
|
443
|
+
${joint
|
|
444
|
+
? html`<button act detach-held ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
445
|
+
${i18next.t('figure.button.detach-held')}
|
|
446
|
+
</button>
|
|
447
|
+
${this.renderDetachWords()}`
|
|
448
|
+
: nothing}
|
|
449
|
+
</div>`
|
|
289
450
|
}
|
|
290
451
|
|
|
291
452
|
/** The same source, the same share, a new difference: a leaf the opening less its gaps (link-dimension's `plus`). */
|
|
@@ -347,14 +508,16 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
347
508
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
348
509
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
349
510
|
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: {} }) }
|
|
511
|
+
this.fasten = { ...f, ...(aim ? placeKeptOf(asset, this.partId, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) }
|
|
351
512
|
}
|
|
352
513
|
|
|
353
514
|
private renderFastening(asset: FigureAsset) {
|
|
354
515
|
const held = heldBy(asset, this.partId)
|
|
355
516
|
const f = this.fasten
|
|
356
517
|
/* 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)
|
|
518
|
+
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one))
|
|
519
|
+
/* Shown, not dropped, and not choosable, with the reason (ADR-0097 §5.5): fastening it would close a loop. */
|
|
520
|
+
const loops = (one: string) => hangsFrom(asset, one, this.partId)
|
|
358
521
|
/* 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
522
|
/* On the mounting plane only the faces it takes: a bottom or a top on the floor, a back on a wall. */
|
|
360
523
|
const plane = mountingPlaneOf(asset)
|
|
@@ -367,72 +530,130 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
367
530
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
368
531
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
369
532
|
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
|
|
533
|
+
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist, f.alignInput) : undefined
|
|
534
|
+
const action =
|
|
535
|
+
f.toPlane || f.target
|
|
536
|
+
? ({
|
|
537
|
+
kind: 'attach',
|
|
538
|
+
part: this.partId,
|
|
539
|
+
face: myFace,
|
|
540
|
+
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
541
|
+
...(f.toPlane ? {} : { facing: f.facing }),
|
|
542
|
+
...(f.gap ? { gap: f.gap } : {}),
|
|
543
|
+
twist: f.twist,
|
|
544
|
+
...(align ? { align } : {}),
|
|
545
|
+
replace: !!held
|
|
546
|
+
} as FigureAuthoringAction)
|
|
547
|
+
: undefined
|
|
548
|
+
if (action) this.fastenCandidate = { action, ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } }) }
|
|
549
|
+
/* What the details hold that is not their default, and where on the face it sits: said while they are folded. */
|
|
550
|
+
const detail = [
|
|
551
|
+
...(f.facing === 'flush' && !f.toPlane ? [i18next.t('figure.label.facing-flush')] : []),
|
|
552
|
+
...Object.entries(align ?? {}).map(([axis, one]) =>
|
|
553
|
+
one === 'min' || one === 'max'
|
|
554
|
+
? i18next.t(`figure.text.fasten-summary-edge-${one}`, { axis: axis.toUpperCase() })
|
|
555
|
+
: typeof one.mm === 'number'
|
|
556
|
+
? i18next.t('figure.text.fasten-summary-off-centre', { axis: axis.toUpperCase(), mm: String(Math.round(one.mm * 10) / 10) })
|
|
557
|
+
: i18next.t('figure.text.fasten-summary-follows', { axis: axis.toUpperCase() })
|
|
558
|
+
),
|
|
559
|
+
...(f.gap ? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })] : []),
|
|
560
|
+
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
561
|
+
]
|
|
562
|
+
/*
|
|
563
|
+
Editing the fastening it has, or fastening it anew: said, with the relation named, so the form is not taken for a
|
|
564
|
+
blank new one while it holds the current (designer 2026-09-27). Applied unchanged, the same fastening is written
|
|
565
|
+
again, fields and all (resize-with-placement.test).
|
|
566
|
+
*/
|
|
567
|
+
const current = asset.joints?.find(j => j.body1.part === this.partId)
|
|
568
|
+
const editing = current
|
|
569
|
+
? i18next.t('figure.text.fasten-editing', {
|
|
570
|
+
face: i18next.t(`figure.label.face-${current.body1.face}`),
|
|
571
|
+
target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
|
|
572
|
+
})
|
|
573
|
+
: i18next.t('figure.text.fasten-new')
|
|
574
|
+
/* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
|
|
575
|
+
if (!current && !this.choosingFastening && !f.target)
|
|
576
|
+
return html`
|
|
577
|
+
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
578
|
+
<button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
|
|
579
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
580
|
+
</button>
|
|
581
|
+
`
|
|
371
582
|
return html`
|
|
583
|
+
<p fasten-mode ?editing=${!!current}>${editing}</p>
|
|
584
|
+
${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
585
|
+
${this.overhangWords.length
|
|
586
|
+
? html`<div overhang role="status">
|
|
587
|
+
${this.overhangWords.map(line => html`<p>${line}</p>`)}
|
|
588
|
+
${current
|
|
589
|
+
? html`<button act @click=${() => this.openDetail()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>`
|
|
590
|
+
: nothing}
|
|
591
|
+
</div>`
|
|
592
|
+
: nothing}
|
|
372
593
|
${held
|
|
373
594
|
? html`<div pair>
|
|
374
595
|
<span>${i18next.t('figure.label.fastened-to')}: <b bound>${'plane' in held ? i18next.t('figure.label.mounting-plane') : held.part}</b></span>
|
|
375
596
|
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
376
597
|
<md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
|
|
377
598
|
</button>
|
|
378
|
-
</div
|
|
599
|
+
</div>
|
|
600
|
+
${this.renderDetachWords()}`
|
|
379
601
|
: nothing}
|
|
380
|
-
<div form>
|
|
381
|
-
<span>${i18next.t('figure.label.my-face')}</span>
|
|
382
|
-
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as FigureFace | 'centre' })}>
|
|
383
|
-
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
384
|
-
</select>
|
|
385
|
-
</div>
|
|
386
|
-
<label toggle>
|
|
387
|
-
<input type="checkbox" .checked=${f.toPlane} @change=${(e: Event) => this.aimAt(asset, { ...f, toPlane: (e.target as HTMLInputElement).checked })} />
|
|
388
|
-
${i18next.t('figure.label.stands-on-plane')}
|
|
389
|
-
</label>
|
|
390
602
|
${f.toPlane
|
|
391
|
-
?
|
|
603
|
+
? html`<p hint>${i18next.t('figure.text.fasten-edit-on-plane')}</p>`
|
|
392
604
|
: html`<div form>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
} as
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
605
|
+
<span>${i18next.t('figure.label.target-part')}</span>
|
|
606
|
+
<select @change=${(e: Event) => this.aimAt(asset, { ...f, target: (e.target as HTMLSelectElement).value })}>
|
|
607
|
+
<option value="" ?selected=${!f.target}>${i18next.t('figure.label.target-part-placeholder')}</option>
|
|
608
|
+
${targets.map(one =>
|
|
609
|
+
loops(one)
|
|
610
|
+
? html`<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
611
|
+
: html`<option value=${one} ?selected=${one === f.target}>${one}</option>`
|
|
612
|
+
)}
|
|
613
|
+
</select>
|
|
614
|
+
</div>
|
|
615
|
+
${f.target
|
|
616
|
+
? html`<div form>
|
|
617
|
+
<span>${i18next.t('figure.label.my-face')}</span>
|
|
618
|
+
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as FigureFace | 'centre' })}>
|
|
619
|
+
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
620
|
+
</select>
|
|
621
|
+
<span>${i18next.t('figure.label.target-face')}</span>
|
|
622
|
+
<select @change=${(e: Event) => this.aimAt(asset, { ...f, targetFace: (e.target as HTMLSelectElement).value as FigureFace })}>
|
|
623
|
+
${theirFaces.map(face => html`<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
624
|
+
</select>
|
|
625
|
+
</div>`
|
|
626
|
+
: html`<p hint>${i18next.t('figure.text.fasten-target-first')}</p>`}`}
|
|
627
|
+
${(f.toPlane || f.target) ? html`<details fasten-detail>
|
|
628
|
+
<summary>${i18next.t('figure.label.fasten-detail')} · <span fasten-summary>${detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')}</span></summary>
|
|
629
|
+
${f.toPlane
|
|
630
|
+
? nothing
|
|
631
|
+
: html`<div form>
|
|
632
|
+
<span>${i18next.t('figure.label.facing')}</span>
|
|
633
|
+
<select @change=${(e: Event) => (this.fasten = { ...f, facing: (e.target as HTMLSelectElement).value as 'meet' | 'flush' })}>
|
|
634
|
+
<option value="meet" ?selected=${f.facing === 'meet'}>${i18next.t('figure.label.facing-meet')}</option>
|
|
635
|
+
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
636
|
+
</select>
|
|
637
|
+
</div>`}
|
|
638
|
+
${this.renderAlignment(f.toPlane || f.target ? alignAxesOf(asset, this.partId, aim, !!f.twist) : [])}
|
|
639
|
+
<div form>
|
|
640
|
+
<span>${i18next.t('figure.label.gap')}</span>
|
|
641
|
+
<input type="number" step="any" .value=${String(f.gap)} @change=${(e: Event) => (this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
642
|
+
<span>${i18next.t('figure.label.twist')}</span>
|
|
643
|
+
<input type="number" step="any" .value=${String(f.twist)} @change=${(e: Event) => (this.fasten = { ...f, twist: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
644
|
+
</div>
|
|
645
|
+
</details>
|
|
646
|
+
${action && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
|
|
647
|
+
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
648
|
+
<md-icon>link</md-icon>${i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')}
|
|
649
|
+
</button>` : nothing}
|
|
433
650
|
`
|
|
434
651
|
}
|
|
435
652
|
|
|
653
|
+
private renderDetachWords() {
|
|
654
|
+
return this.detachWords.length ? html`<ul detach-words>${this.detachWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
|
|
655
|
+
}
|
|
656
|
+
|
|
436
657
|
/**
|
|
437
658
|
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
438
659
|
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
@@ -451,6 +672,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
451
672
|
? html`<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
452
673
|
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>`
|
|
453
674
|
: nothing}
|
|
675
|
+
${f.alignInput?.[axis]
|
|
676
|
+
? html`<option value="input" ?selected=${how === 'input'}>${i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis]!, key => i18next.t(key)) })}</option>`
|
|
677
|
+
: nothing}
|
|
454
678
|
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
455
679
|
${this.asset && hasInstanceSize(this.asset, axis)
|
|
456
680
|
? html`<option value="share" ?selected=${how === 'share'}>${i18next.t('figure.label.align-share')}</option>`
|
|
@@ -468,37 +692,59 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
468
692
|
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
469
693
|
* (the editor's) shows it and takes it away.
|
|
470
694
|
*/
|
|
695
|
+
private renderPreviewWords() {
|
|
696
|
+
return this.previewWords.length ? html`<ul preview-words role="status">${this.previewWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* A row (ADR-0097 §5.5): said as what it makes, "arranged in a row". Where it cannot be made here, why, rather than
|
|
701
|
+
* nothing. Filling the length, the centre spacing is the author's; a count of their own is spread evenly over the
|
|
702
|
+
* length -- figure-model draws it so, and the form does not ask for a spacing it would not use.
|
|
703
|
+
*/
|
|
471
704
|
private renderRow(asset: FigureAsset) {
|
|
705
|
+
if (rowOf(asset, this.partId)) return nothing
|
|
706
|
+
const blocked = rowBlockOf(asset, this.partId)
|
|
472
707
|
const axes = rowAxesOf(asset, this.partId)
|
|
473
|
-
if (!axes.length) return nothing
|
|
474
708
|
const r = this.row
|
|
709
|
+
if (blocked || !axes.length)
|
|
710
|
+
return html`<section row>
|
|
711
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
712
|
+
<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>
|
|
713
|
+
${blocked === 'unfastened'
|
|
714
|
+
? html`<button act @click=${() => this.openFastening(asset)}><md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}</button>`
|
|
715
|
+
: nothing}
|
|
716
|
+
</section>`
|
|
475
717
|
const axis = axes.includes(r.axis as Axis) ? (r.axis as Axis) : axes[0]!
|
|
476
|
-
const ok = r.pitch > 0
|
|
477
|
-
|
|
718
|
+
const ok = r.fills ? r.pitch > 0 : Number.isSafeInteger(r.count) && r.count >= 1
|
|
719
|
+
const action = ok
|
|
720
|
+
? ({ kind: 'make-repeat', part: this.partId, axis, pitch: r.fills ? r.pitch : Math.max(r.pitch, 1), ...(r.fills ? {} : { count: r.count }) } as FigureAuthoringAction)
|
|
721
|
+
: undefined
|
|
722
|
+
this.rowCandidate = action
|
|
723
|
+
const set = (next: typeof r) => ((this.row = next), (this.working = 'row'))
|
|
724
|
+
return html`<section row @focusin=${() => (this.working = 'row')}>
|
|
478
725
|
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
479
726
|
<div form>
|
|
480
727
|
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
481
|
-
<select @change=${(e: Event) => (
|
|
728
|
+
<select @change=${(e: Event) => set({ ...r, axis: (e.target as HTMLSelectElement).value as Axis })}>
|
|
482
729
|
${axes.map(a => html`<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}
|
|
483
730
|
</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
731
|
</div>
|
|
487
|
-
<label
|
|
488
|
-
<input type="
|
|
489
|
-
|
|
490
|
-
</
|
|
732
|
+
<div row-basis role="radiogroup" aria-label=${i18next.t('figure.label.row-basis')}>
|
|
733
|
+
<label><input type="radio" name="row-basis" .checked=${r.fills} @change=${() => set({ ...r, fills: true })} />${i18next.t('figure.label.repeat-fills')}</label>
|
|
734
|
+
<label><input type="radio" name="row-basis" .checked=${!r.fills} @change=${() => set({ ...r, fills: false })} />${i18next.t('figure.label.row-by-count')}</label>
|
|
735
|
+
</div>
|
|
491
736
|
${r.fills
|
|
492
|
-
?
|
|
737
|
+
? html`<div form>
|
|
738
|
+
<span>${i18next.t('figure.label.repeat-pitch')} · mm</span>
|
|
739
|
+
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e: Event) => set({ ...r, pitch: Number((e.target as HTMLInputElement).value) })} />
|
|
740
|
+
</div>`
|
|
493
741
|
: 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
|
-
>
|
|
742
|
+
<span>${i18next.t('figure.label.repeat-count')}</span>
|
|
743
|
+
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e: Event) => set({ ...r, count: Number((e.target as HTMLInputElement).value) })} />
|
|
744
|
+
</div>
|
|
745
|
+
<p hint>${i18next.t('figure.text.row-count-spreads')}</p>`}
|
|
746
|
+
${action && this.working === 'row' ? this.renderPreviewWords() : nothing}
|
|
747
|
+
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
502
748
|
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
503
749
|
</button>
|
|
504
750
|
</section>`
|
|
@@ -551,7 +797,21 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
551
797
|
const now = motionOf(asset, this.partId)
|
|
552
798
|
const joint = asset.joints?.find(j => j.body1.part === this.partId)
|
|
553
799
|
if (now && !(this.editingMotion && joint)) {
|
|
554
|
-
|
|
800
|
+
/* Which part moves, against what, which way, how far -- by name (ADR-0097 §5.5). */
|
|
801
|
+
const against = joint ? ('plane' in joint.body0 ? i18next.t('figure.label.mounting-plane') : joint.body0.part) : ''
|
|
802
|
+
const ends = this.motionEnds
|
|
803
|
+
return html`<p motion-summary>
|
|
804
|
+
${i18next.t('figure.text.motion-summary', {
|
|
805
|
+
part: this.partId,
|
|
806
|
+
against,
|
|
807
|
+
kind: i18next.t('figure.label.motion-' + now.kind),
|
|
808
|
+
axis: String(now.axis).toUpperCase()
|
|
809
|
+
})}
|
|
810
|
+
</p>
|
|
811
|
+
${ends
|
|
812
|
+
? html`<p hint motion-ends>${i18next.t('figure.text.motion-ends-shown', { min: String(ends.min), max: String(ends.max), unit: ends.unit })}</p>`
|
|
813
|
+
: nothing}
|
|
814
|
+
<div pair>
|
|
555
815
|
<span>
|
|
556
816
|
${i18next.t('figure.label.motion-' + now.kind)} · ${String(now.axis).toUpperCase()}
|
|
557
817
|
${now.travel
|
|
@@ -566,7 +826,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
566
826
|
<button
|
|
567
827
|
act
|
|
568
828
|
?disabled=${this.locked || !joint}
|
|
569
|
-
@click=${() => joint && this.command({ kind: 'set-joint', joint:
|
|
829
|
+
@click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}
|
|
570
830
|
>
|
|
571
831
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
572
832
|
</button>
|
|
@@ -584,7 +844,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
584
844
|
</div>`
|
|
585
845
|
}
|
|
586
846
|
/* A joint needs a parent (URDF): a part fastened to nothing is fastened first. */
|
|
587
|
-
if (!joint)
|
|
847
|
+
if (!joint)
|
|
848
|
+
return html`<p hint>${i18next.t('figure.text.fasten-before-moving')}</p>
|
|
849
|
+
<button act go-fasten @click=${() => this.dispatchEvent(new CustomEvent('go-fasten', { bubbles: true, composed: true }))}>
|
|
850
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}
|
|
851
|
+
</button>`
|
|
588
852
|
const m = this.motion
|
|
589
853
|
const unit = m.kind === 'slide' ? 'mm' : 'deg'
|
|
590
854
|
/* A follower follows a joint that moves the same way; a choice the kind no longer allows is dropped. */
|
|
@@ -667,4 +931,3 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
667
931
|
this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))
|
|
668
932
|
}
|
|
669
933
|
}
|
|
670
|
-
|