@things-factory/figure-ui 10.1.245 → 10.1.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/align.ts +51 -13
- package/client/modeller/clips.ts +51 -1
- package/client/modeller/data-interface.ts +16 -0
- package/client/modeller/editor-styles.ts +6 -0
- package/client/modeller/editor.ts +112 -25
- package/client/modeller/fasten-form.ts +8 -1
- package/client/modeller/fasten-geometry.ts +24 -2
- package/client/modeller/figure-clips.ts +137 -4
- package/client/modeller/figure-fasten-dialog.ts +1 -0
- package/client/modeller/figure-fasten-drawing.ts +11 -5
- package/client/modeller/figure-links.ts +25 -0
- package/client/modeller/figure-material-panel.ts +54 -4
- package/client/modeller/figure-parts.ts +7 -2
- package/client/modeller/figure-pipe-panel.ts +215 -0
- package/client/modeller/figure-preview.ts +125 -4
- package/client/modeller/figure-section-editor.ts +36 -3
- package/client/modeller/links.ts +24 -2
- package/client/modeller/material-edit.ts +9 -0
- package/client/modeller/maturity.ts +15 -4
- package/client/modeller/palette.ts +9 -0
- package/client/modeller/pipe-path.ts +21 -0
- package/client/modeller/refusals.ts +66 -1
- package/client/modeller/section-controls.ts +2 -1
- package/client/modeller/section-drawing.ts +20 -1
- package/client/modeller/starting-shape.ts +8 -5
- package/dist-client/modeller/align.d.ts +28 -7
- package/dist-client/modeller/align.js +37 -12
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +24 -0
- package/dist-client/modeller/clips.js +53 -1
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +6 -0
- package/dist-client/modeller/data-interface.js +17 -0
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +6 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +10 -1
- package/dist-client/modeller/editor.js +117 -26
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +2 -0
- package/dist-client/modeller/fasten-form.js +5 -1
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +8 -1
- package/dist-client/modeller/fasten-geometry.js +22 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +137 -4
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +1 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +3 -1
- package/dist-client/modeller/figure-fasten-drawing.js +13 -5
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +5 -0
- package/dist-client/modeller/figure-links.js +22 -0
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +2 -0
- package/dist-client/modeller/figure-material-panel.js +53 -4
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +5 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-pipe-panel.d.ts +29 -0
- package/dist-client/modeller/figure-pipe-panel.js +221 -0
- package/dist-client/modeller/figure-pipe-panel.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +8 -0
- package/dist-client/modeller/figure-preview.js +128 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +5 -0
- package/dist-client/modeller/figure-section-editor.js +33 -3
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -0
- package/dist-client/modeller/links.js +17 -2
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +8 -0
- package/dist-client/modeller/material-edit.js +8 -0
- package/dist-client/modeller/material-edit.js.map +1 -1
- package/dist-client/modeller/maturity.js +16 -5
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/palette.js +9 -0
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/pipe-path.d.ts +19 -0
- package/dist-client/modeller/pipe-path.js +14 -0
- package/dist-client/modeller/pipe-path.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +12 -1
- package/dist-client/modeller/refusals.js +67 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.js +2 -1
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +5 -0
- package/dist-client/modeller/section-drawing.js +19 -1
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/starting-shape.d.ts +9 -2
- package/dist-client/modeller/starting-shape.js +8 -3
- package/dist-client/modeller/starting-shape.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/align.test.ts +28 -6
- package/test/browser/editing-page.js +21 -1
- package/test/browser/editing.check.mjs +124 -0
- package/test/browser/links.check.mjs +49 -8
- package/test/browser/modeller.check.mjs +27 -1
- package/test/browser/part-ids.check.mjs +60 -3
- package/test/browser/parts.check.mjs +2 -2
- package/test/browser/places-mesh.check.mjs +14 -8
- package/test/browser/screen.check.mjs +54 -1
- package/test/browser/section-page.js +7 -1
- package/test/browser/section.check.mjs +81 -0
- package/test/browser/side-panel-sweep.check.mjs +13 -0
- package/test/clips.test.ts +30 -1
- package/test/data-motion.test.ts +14 -1
- package/test/fasten-geometry.test.ts +13 -2
- package/test/joint-frames.test.ts +12 -2
- package/test/links.test.ts +24 -0
- package/test/material.test.ts +15 -1
- package/test/maturity.test.ts +19 -1
- package/test/palette-icons.test.ts +2 -1
- package/test/pipe-panel.test.ts +33 -0
- package/test/refusals.test.ts +29 -0
- package/test/revision.test.ts +1 -1
- package/test/section-editor.test.ts +17 -1
- package/translations/en.json +86 -3
- package/translations/es.json +86 -3
- package/translations/ja.json +86 -3
- package/translations/ko.json +86 -3
- package/translations/ms.json +86 -3
- package/translations/zh.json +86 -3
|
@@ -7,12 +7,14 @@ import { i18next, localize } from '@operato/i18n'
|
|
|
7
7
|
import { unitWord } from './unit-words.js'
|
|
8
8
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
|
-
import { clipGroupsOf, clipsOf, freeGroupIdOf, groupEdit, poseAt, trackEdit, undrivenOf, withKey, withoutKey, type Clip, type ClipGroup, type ClipTrack } from './clips.js'
|
|
10
|
+
import { clipGroupsOf, withKeyChanged, clipsOf, copiedTrackEdit, copyTargetsOf, freeClipNameOf, freeGroupIdOf, groupEdit, poseAt, shiftable, trackEdit, undrivenOf, withKey, withoutKey, type Clip, type ClipGroup, type ClipTrack } from './clips.js'
|
|
11
11
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
12
12
|
import { HelpToggles } from './help.js'
|
|
13
13
|
|
|
14
14
|
/** A value as a person reads it: at most three decimals, the float's last digits (14.399999999999999) left out. */
|
|
15
15
|
const shown = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
16
|
+
/* A number field's value: no grouping, to a thousandth. */
|
|
17
|
+
const typed = (value: number) => String(Math.round(value * 1000) / 1000)
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* 클립 — controls played by keys over time. A clip is one clock; each track plays one
|
|
@@ -55,7 +57,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
55
57
|
*/
|
|
56
58
|
div[track-head] {
|
|
57
59
|
display: grid;
|
|
58
|
-
grid-template-columns: minmax(0, 1fr) 30px;
|
|
60
|
+
grid-template-columns: minmax(0, 1fr) 30px 30px;
|
|
59
61
|
gap: 4px 6px;
|
|
60
62
|
align-items: center;
|
|
61
63
|
}
|
|
@@ -70,6 +72,19 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
70
72
|
margin: 0;
|
|
71
73
|
padding: 0;
|
|
72
74
|
}
|
|
75
|
+
/* A track copied onto a control, moved round its loop: under its head, in the panel's form look. */
|
|
76
|
+
div[copy-form] {
|
|
77
|
+
display: grid;
|
|
78
|
+
gap: 6px;
|
|
79
|
+
padding: 6px 8px;
|
|
80
|
+
border-radius: 6px;
|
|
81
|
+
background: var(--md-sys-color-surface-container);
|
|
82
|
+
}
|
|
83
|
+
div[copy-form] div[pair] {
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: 6px;
|
|
86
|
+
justify-content: flex-end;
|
|
87
|
+
}
|
|
73
88
|
div[keys] {
|
|
74
89
|
display: flex;
|
|
75
90
|
flex-wrap: wrap;
|
|
@@ -84,6 +99,34 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
84
99
|
background: var(--md-sys-color-surface-container);
|
|
85
100
|
font-variant-numeric: tabular-nums;
|
|
86
101
|
}
|
|
102
|
+
/* A key's moment and value, typed in the chip: as narrow as their numbers, on the chip's ground. */
|
|
103
|
+
span[key] input {
|
|
104
|
+
width: 44px;
|
|
105
|
+
min-width: 0;
|
|
106
|
+
height: 20px;
|
|
107
|
+
padding: 0 2px;
|
|
108
|
+
font: inherit;
|
|
109
|
+
text-align: right;
|
|
110
|
+
font-variant-numeric: tabular-nums;
|
|
111
|
+
color: inherit;
|
|
112
|
+
background: transparent;
|
|
113
|
+
border: none;
|
|
114
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
115
|
+
}
|
|
116
|
+
span[key] input[readonly] {
|
|
117
|
+
border-bottom-color: transparent;
|
|
118
|
+
}
|
|
119
|
+
label[at] {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 2px;
|
|
123
|
+
font-variant-numeric: tabular-nums;
|
|
124
|
+
}
|
|
125
|
+
label[at] input {
|
|
126
|
+
width: 52px;
|
|
127
|
+
min-width: 0;
|
|
128
|
+
text-align: right;
|
|
129
|
+
}
|
|
87
130
|
span[key] button {
|
|
88
131
|
width: 22px;
|
|
89
132
|
height: 22px;
|
|
@@ -107,6 +150,8 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
107
150
|
@state() private at: Record<string, number> = {}
|
|
108
151
|
@state() private adding = { state: '', clip: '', name: '', duration: 2, group: '' }
|
|
109
152
|
@state() private grouping = { clip: '', label: '' }
|
|
153
|
+
/** The track whose copy is being set up: onto which control, how many seconds later. */
|
|
154
|
+
@state() private copying?: { clip: string; state: string; target: string; seconds: number }
|
|
110
155
|
|
|
111
156
|
render() {
|
|
112
157
|
const asset = this.asset
|
|
@@ -128,6 +173,8 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
128
173
|
private renderClip(clip: Clip) {
|
|
129
174
|
const at = Math.min(this.at[clip.name] ?? 0, clip.duration)
|
|
130
175
|
const number = (e: Event) => (e.target as HTMLInputElement).valueAsNumber
|
|
176
|
+
/* A copy plays the same controls: only clips of one motion group may (ADR-0051 §3.2). */
|
|
177
|
+
const grouped = !!this.asset?.motionGroups?.some(g => g.clips.includes(clip.name))
|
|
131
178
|
return html`<section clip>
|
|
132
179
|
<div form>
|
|
133
180
|
<span>${i18next.t('figure.label.clip-name')}</span>
|
|
@@ -135,6 +182,10 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
135
182
|
<span>${i18next.t('figure.label.clip-duration')}</span>
|
|
136
183
|
<input type="number" min="0.1" step="any" .value=${String(clip.duration)} ?disabled=${this.locked} @change=${(e: Event) => this.edit({ kind: 'set-clip', clip: clip.name, duration: number(e) })} />
|
|
137
184
|
</div>
|
|
185
|
+
<button act copy-clip ?disabled=${this.locked || !grouped} title=${i18next.t(grouped ? 'figure.text.copy-clip-explained' : 'figure.text.copy-clip-needs-group')}
|
|
186
|
+
@click=${() => this.edit({ kind: 'copy-clip', clip: clip.name, name: freeClipNameOf(this.asset!, clip.name) })}>
|
|
187
|
+
<md-icon>content_copy</md-icon>${i18next.t('figure.button.copy-clip')}
|
|
188
|
+
</button>
|
|
138
189
|
<div row>
|
|
139
190
|
<input
|
|
140
191
|
type="range"
|
|
@@ -147,7 +198,18 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
147
198
|
@input=${(e: Event) => this.scrub(clip, number(e), true)}
|
|
148
199
|
@change=${(e: Event) => this.scrub(clip, number(e))}
|
|
149
200
|
/>
|
|
150
|
-
<
|
|
201
|
+
<label at>
|
|
202
|
+
<input
|
|
203
|
+
type="number"
|
|
204
|
+
min="0"
|
|
205
|
+
max=${clip.duration}
|
|
206
|
+
step="any"
|
|
207
|
+
.value=${String(Math.round(at * 1000) / 1000)}
|
|
208
|
+
?disabled=${this.locked}
|
|
209
|
+
aria-label=${i18next.t('figure.label.clip-time')}
|
|
210
|
+
@change=${(e: Event) => this.scrub(clip, Math.min(Math.max(number(e), 0), clip.duration))}
|
|
211
|
+
/>s
|
|
212
|
+
</label>
|
|
151
213
|
</div>
|
|
152
214
|
${clip.tracks.map(track => this.renderTrack(clip, track, at))}
|
|
153
215
|
</section>`
|
|
@@ -166,14 +228,38 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
166
228
|
<option value="wrap" ?selected=${track.loop === 'wrap'}>${i18next.t('figure.label.clip-loop-wrap')}</option>
|
|
167
229
|
<option value="hold" ?selected=${track.loop === 'hold'}>${i18next.t('figure.label.clip-loop-hold')}</option>
|
|
168
230
|
</select>
|
|
231
|
+
<button act copy-track ?disabled=${this.locked} title=${i18next.t('figure.button.copy-track')} aria-label=${i18next.t('figure.button.copy-track')}
|
|
232
|
+
@click=${() => (this.copying = this.copying?.clip === clip.name && this.copying.state === track.state ? undefined : { clip: clip.name, state: track.state, target: track.state, seconds: 0 })}>
|
|
233
|
+
<md-icon>content_copy</md-icon>
|
|
234
|
+
</button>
|
|
169
235
|
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-track')} @click=${() => this.edit({ kind: 'remove-driver', state: track.state, clip: clip.name })}>
|
|
170
236
|
<md-icon>close</md-icon>
|
|
171
237
|
</button>
|
|
172
238
|
</div>
|
|
239
|
+
${this.copying?.clip === clip.name && this.copying.state === track.state ? this.renderCopy(clip, track) : nothing}
|
|
173
240
|
<div keys>
|
|
174
241
|
${track.keys.map(
|
|
175
242
|
(key, i) => html`<span key
|
|
176
|
-
|
|
243
|
+
><input
|
|
244
|
+
type="number"
|
|
245
|
+
step="any"
|
|
246
|
+
min="0"
|
|
247
|
+
max=${clip.duration}
|
|
248
|
+
?readonly=${i === 0}
|
|
249
|
+
?disabled=${this.locked}
|
|
250
|
+
title=${i === 0 ? i18next.t('figure.text.key-starts-the-clip') : ''}
|
|
251
|
+
aria-label=${`${i18next.t('figure.label.key-seconds')} ${i + 1}`}
|
|
252
|
+
.value=${typed(key.seconds)}
|
|
253
|
+
@change=${(e: Event) => this.changeKey(clip, track, i, { seconds: (e.target as HTMLInputElement).valueAsNumber }, e)}
|
|
254
|
+
/>s →
|
|
255
|
+
<input
|
|
256
|
+
type="number"
|
|
257
|
+
step="any"
|
|
258
|
+
?disabled=${this.locked}
|
|
259
|
+
aria-label=${`${i18next.t('figure.label.key-value')} ${i + 1}`}
|
|
260
|
+
.value=${typed(key.value)}
|
|
261
|
+
@change=${(e: Event) => this.changeKey(clip, track, i, { value: (e.target as HTMLInputElement).valueAsNumber }, e)}
|
|
262
|
+
/>
|
|
177
263
|
<button
|
|
178
264
|
?disabled=${this.locked || !withoutKey(track.keys, i)}
|
|
179
265
|
title=${i18next.t('figure.button.remove-key')}
|
|
@@ -195,6 +281,41 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
195
281
|
</div>`
|
|
196
282
|
}
|
|
197
283
|
|
|
284
|
+
/**
|
|
285
|
+
* A track onto a control -- itself, or another -- moved round its loop by some seconds: one leg's curve given to the
|
|
286
|
+
* next on its own beat (robot dog 2026-11-03). Only a wrapping track moves round; any track is copied.
|
|
287
|
+
*/
|
|
288
|
+
private renderCopy(clip: Clip, track: ClipTrack) {
|
|
289
|
+
const c = this.copying!
|
|
290
|
+
const asset = this.asset!
|
|
291
|
+
const targets = copyTargetsOf(asset, clip.name)
|
|
292
|
+
const moves = shiftable(track)
|
|
293
|
+
const seconds = moves && Number.isFinite(c.seconds) ? c.seconds : 0
|
|
294
|
+
const same = c.target === track.state && !seconds
|
|
295
|
+
return html`<div copy-form>
|
|
296
|
+
<div form>
|
|
297
|
+
<span>${i18next.t('figure.label.copy-track-onto')}</span>
|
|
298
|
+
<select aria-label=${i18next.t('figure.label.copy-track-onto')} @change=${(e: Event) => (this.copying = { ...c, target: (e.target as HTMLSelectElement).value })}>
|
|
299
|
+
${targets.map(id => html`<option value=${id} ?selected=${id === c.target}>${id === track.state ? i18next.t('figure.label.copy-track-itself') : (asset.stateInputs?.[id]?.label ?? id)}</option>`)}
|
|
300
|
+
</select>
|
|
301
|
+
<span>${i18next.t('figure.label.copy-track-later')}</span>
|
|
302
|
+
<label at title=${moves ? '' : i18next.t('figure.text.copy-track-no-loop')}>
|
|
303
|
+
<input type="number" step="any" aria-label=${i18next.t('figure.label.copy-track-later')} ?disabled=${!moves} .value=${typed(seconds)}
|
|
304
|
+
@change=${(e: Event) => (this.copying = { ...c, seconds: (e.target as HTMLInputElement).valueAsNumber })} />s
|
|
305
|
+
<small>/ ${typed(clip.duration)}s</small>
|
|
306
|
+
</label>
|
|
307
|
+
</div>
|
|
308
|
+
<div pair>
|
|
309
|
+
<button act ?disabled=${this.locked || same}
|
|
310
|
+
@click=${() => {
|
|
311
|
+
this.edit(copiedTrackEdit(asset, clip, track, c.target, seconds))
|
|
312
|
+
this.copying = undefined
|
|
313
|
+
}}>${i18next.t(c.target === track.state ? 'figure.button.shift-track' : 'figure.button.copy-track-apply')}</button>
|
|
314
|
+
<button act @click=${() => (this.copying = undefined)}>${i18next.t('figure.button.cancel')}</button>
|
|
315
|
+
</div>
|
|
316
|
+
</div>`
|
|
317
|
+
}
|
|
318
|
+
|
|
198
319
|
private renderAdd(asset: FigureAsset, clips: Clip[]) {
|
|
199
320
|
const a = this.adding
|
|
200
321
|
const joining = clips.find(c => c.name === a.clip)
|
|
@@ -370,6 +491,18 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
370
491
|
this.dispatchEvent(new CustomEvent('states', { detail: { changes }, bubbles: true, composed: true }))
|
|
371
492
|
}
|
|
372
493
|
|
|
494
|
+
/** A key's moment or value typed: one edit of the track; a moment the track cannot take puts the field back. */
|
|
495
|
+
private changeKey(clip: Clip, track: ClipTrack, index: number, patch: Partial<{ seconds: number; value: number }>, e: Event) {
|
|
496
|
+
const keys = withKeyChanged(track.keys, index, patch, clip.duration)
|
|
497
|
+
if (!keys) {
|
|
498
|
+
const field = e.target as HTMLInputElement
|
|
499
|
+
const was = track.keys[index]!
|
|
500
|
+
field.value = 'seconds' in patch ? typed(was.seconds) : typed(was.value)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
this.edit(trackEdit(clip, track, { keys }))
|
|
504
|
+
}
|
|
505
|
+
|
|
373
506
|
private edit(edit: unknown) {
|
|
374
507
|
this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit }, bubbles: true, composed: true }))
|
|
375
508
|
}
|
|
@@ -197,6 +197,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
197
197
|
.target=${f.toPlane ? '' : f.target}
|
|
198
198
|
.planeWord=${planeWord}
|
|
199
199
|
.faces=${plan.theirFaces}
|
|
200
|
+
.drawnFaces=${plan.drawnFaces}
|
|
200
201
|
.face=${plan.targetFace}
|
|
201
202
|
.modes=${modes}
|
|
202
203
|
.mode=${plan.mode}
|
|
@@ -161,6 +161,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
161
161
|
/** The plane's name by the figure's placement: 바닥 · 천장 · 벽 · 중심. */
|
|
162
162
|
@property() planeWord = ''
|
|
163
163
|
@property({ attribute: false }) faces: string[] = []
|
|
164
|
+
/** The target's own face at each place of the box, as it is drawn: a turned target's faces where they are seen. */
|
|
165
|
+
@property({ attribute: false }) drawnFaces?: Partial<Record<FigureFace, FigureFace>>
|
|
164
166
|
@property() face = ''
|
|
165
167
|
@property({ attribute: false }) modes: FastenMode[] = ['meet', 'flush']
|
|
166
168
|
@property() mode: FastenMode = 'meet'
|
|
@@ -213,22 +215,26 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
213
215
|
this.change({ targetFace: f })
|
|
214
216
|
this.released()
|
|
215
217
|
}
|
|
216
|
-
|
|
218
|
+
/* The face at each place as the target is drawn: its own name, pressed as its own. */
|
|
219
|
+
const own = (place: FigureFace) => this.drawnFaces?.[place] ?? place
|
|
220
|
+
const seen = SEEN.map(([place, d]) => {
|
|
221
|
+
const f = own(place)
|
|
217
222
|
const on = !this.surface && this.face === f
|
|
218
|
-
return svg`<path face=${f} d=${d} @click=${pick(f)}
|
|
223
|
+
return svg`<path face=${f} place=${place} d=${d} @click=${pick(f)}
|
|
219
224
|
style=${`cursor:${has(f) ? 'pointer' : 'default'};fill:${on ? 'var(--md-sys-color-primary-container)' : 'var(--md-sys-color-surface-container)'};fill-opacity:${has(f) ? 1 : 0.4};stroke:${on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'};stroke-width:${on ? 2 : 1}`}>
|
|
220
225
|
<title>${i18next.t('figure.label.face-' + f)}</title></path>`
|
|
221
226
|
})
|
|
222
|
-
const hidden = HIDDEN.map(([
|
|
227
|
+
const hidden = HIDDEN.map(([place, x, y]) => {
|
|
228
|
+
const f = own(place)
|
|
223
229
|
const on = !this.surface && this.face === f
|
|
224
|
-
return svg`<g face=${f} @click=${pick(f)} style=${`cursor:${has(f) ? 'pointer' : 'default'};opacity:${has(f) ? 1 : 0.35}`}>
|
|
230
|
+
return svg`<g face=${f} place=${place} @click=${pick(f)} style=${`cursor:${has(f) ? 'pointer' : 'default'};opacity:${has(f) ? 1 : 0.35}`}>
|
|
225
231
|
<rect x=${x - 26} y=${y - 10} width="52" height="20" rx="10"
|
|
226
232
|
style=${`fill:${on ? 'var(--md-sys-color-primary-container)' : 'transparent'};stroke:${on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'};stroke-dasharray:${on ? '' : '3 2'}`} />
|
|
227
233
|
<text x=${x} y=${y + 4} text-anchor="middle" font-size="11" style=${`fill:${on ? 'var(--md-sys-color-on-primary-container)' : 'var(--md-sys-color-on-surface-variant)'}`}>${i18next.t('figure.label.face-' + f)}</text>
|
|
228
234
|
</g>`
|
|
229
235
|
})
|
|
230
236
|
/* A shape's faces beyond its six outer ones -- a door frame's opening -- are tags of their own under the box. */
|
|
231
|
-
const others = this.faces.filter(f => !SEEN.some(([s]) => s === f) && !HIDDEN.some(([h]) => h === f))
|
|
237
|
+
const others = this.faces.filter(f => !SEEN.some(([s]) => own(s) === f) && !HIDDEN.some(([h]) => own(h) === f))
|
|
232
238
|
/* A sphere has no face: no box of faces it has not, only its surface. */
|
|
233
239
|
return html`${this.faces.length
|
|
234
240
|
? svg`<svg viewBox="-20 0 ${PANEL_W} 126" role="img" aria-label=${this.target}>
|
|
@@ -739,6 +739,30 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
739
739
|
</div>`
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
+
/**
|
|
743
|
+
* The point a turn passes (ADR-0159): the axis moved off the joint, the part left where it is -- a hip turning about
|
|
744
|
+
* the middle of its round cover, not its edge. Before, a hidden part stood in for the point (robot dog, 2026-11-03).
|
|
745
|
+
*/
|
|
746
|
+
private renderThrough(m: MotionForm) {
|
|
747
|
+
const typed = (axis: Axis) => (e: Event) => (this.motion = { ...m, through: { ...m.through, [axis]: Number((e.target as HTMLInputElement).value) || 0 } })
|
|
748
|
+
return html`<div vector-row>
|
|
749
|
+
<label title=${i18next.t('figure.text.motion-through-explained')}>${i18next.t('figure.label.motion-through')} <span unit>(mm)</span></label>
|
|
750
|
+
<div triple>
|
|
751
|
+
${AXES.map(axis =>
|
|
752
|
+
m.throughFollows?.[axis] !== undefined
|
|
753
|
+
? html`<div cell held title=${i18next.t('figure.text.motion-through-follows')}>
|
|
754
|
+
<span axis>${axis.toUpperCase()}</span>
|
|
755
|
+
<input type="text" readonly .value=${'—'} aria-label=${`${i18next.t('figure.label.motion-through')} ${axis.toUpperCase()} · ${i18next.t('figure.text.motion-through-follows')}`} />
|
|
756
|
+
</div>`
|
|
757
|
+
: html`<div cell>
|
|
758
|
+
<span axis>${axis.toUpperCase()}</span>
|
|
759
|
+
<input type="number" step="any" aria-label=${`${i18next.t('figure.label.motion-through')} ${axis.toUpperCase()}`} .value=${String(m.through?.[axis] ?? 0)} @change=${typed(axis)} />
|
|
760
|
+
</div>`
|
|
761
|
+
)}
|
|
762
|
+
</div>
|
|
763
|
+
</div>`
|
|
764
|
+
}
|
|
765
|
+
|
|
742
766
|
private renderMotion(asset: FigureAsset) {
|
|
743
767
|
const now = motionOf(asset, this.partId)
|
|
744
768
|
const joint = asset.joints?.find(j => j.body1.part === this.partId)
|
|
@@ -886,6 +910,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
886
910
|
<input type="number" step="any" .value=${String(m.max)} @change=${(e: Event) => (this.motion = { ...m, max: Number((e.target as HTMLInputElement).value) })} />
|
|
887
911
|
</div>`}
|
|
888
912
|
</div>`}
|
|
913
|
+
${m.kind === 'slide' ? nothing : this.renderThrough(m)}
|
|
889
914
|
<button
|
|
890
915
|
act
|
|
891
916
|
?disabled=${this.locked || !!refusal}
|
|
@@ -7,7 +7,7 @@ import { activePalette } from '@hatiolab/things-scene'
|
|
|
7
7
|
import type { FigureAssetGeometry, FigureMaterial, FigurePartEdit } from '@hatiolab/figure-model'
|
|
8
8
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
9
9
|
import { isRoundBody } from './starting-shape.js'
|
|
10
|
-
import { glowOf, materialEditOf, offeredPaletteOf } from './material-edit.js'
|
|
10
|
+
import { glowOf, materialEditOf, offeredPaletteOf, ownColourOf } from './material-edit.js'
|
|
11
11
|
|
|
12
12
|
/** The purpose each palette role is for, by its token. */
|
|
13
13
|
const PURPOSE: Record<string, string> = {
|
|
@@ -80,6 +80,39 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
80
80
|
background: var(--swatch);
|
|
81
81
|
cursor: pointer;
|
|
82
82
|
}
|
|
83
|
+
/*
|
|
84
|
+
The colour picked by its value: a swatch's size, told apart from the roles by a ring of every hue (the usual mark of
|
|
85
|
+
「any colour」). A colour picked joins the swatches before it, chosen there as any is. The browser's picker lies over
|
|
86
|
+
the ring, unseen.
|
|
87
|
+
*/
|
|
88
|
+
label[own-colour] {
|
|
89
|
+
position: relative;
|
|
90
|
+
display: inline-grid;
|
|
91
|
+
place-items: center;
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
width: 22px;
|
|
94
|
+
height: 22px;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
background: conic-gradient(#f44336, #ffeb3b, #4caf50, #00bcd4, #3f51b5, #e91e63, #f44336);
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
}
|
|
99
|
+
label[own-colour]::after {
|
|
100
|
+
content: '';
|
|
101
|
+
width: 12px;
|
|
102
|
+
height: 12px;
|
|
103
|
+
border-radius: 50%;
|
|
104
|
+
background: var(--md-sys-color-surface);
|
|
105
|
+
}
|
|
106
|
+
label[own-colour] input {
|
|
107
|
+
position: absolute;
|
|
108
|
+
inset: 0;
|
|
109
|
+
width: 100%;
|
|
110
|
+
height: 100%;
|
|
111
|
+
padding: 0;
|
|
112
|
+
border: 0;
|
|
113
|
+
opacity: 0;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
}
|
|
83
116
|
button[swatch][on] {
|
|
84
117
|
outline: 2px solid var(--md-sys-color-primary);
|
|
85
118
|
outline-offset: 1px;
|
|
@@ -194,7 +227,18 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
194
227
|
<h3>${i18next.t('figure.label.part-material')}</h3>
|
|
195
228
|
<div field>
|
|
196
229
|
<label>${i18next.t('figure.label.palette-role')}</label>
|
|
197
|
-
<div swatches
|
|
230
|
+
<div swatches>
|
|
231
|
+
${this.swatches(painted, material.token, token => this.put({ token }))}
|
|
232
|
+
<label own-colour title=${i18next.t('figure.text.own-colour-explained')}>
|
|
233
|
+
<input
|
|
234
|
+
type="color"
|
|
235
|
+
?disabled=${this.locked}
|
|
236
|
+
aria-label=${i18next.t('figure.label.own-colour')}
|
|
237
|
+
.value=${this.offered()[material.token] ?? '#9a9a9a'}
|
|
238
|
+
@change=${(e: Event) => this.pickOwn((e.target as HTMLInputElement).value)}
|
|
239
|
+
/>
|
|
240
|
+
</label>
|
|
241
|
+
</div>
|
|
198
242
|
${lit.length
|
|
199
243
|
? html`<small>${i18next.t('figure.label.status-colors')}</small>
|
|
200
244
|
<div swatches>${this.swatches(lit, material.token, token => this.put({ token }))}</div>`
|
|
@@ -314,9 +358,15 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
314
358
|
this.put(glowOf(this.body!.material, intensity), live)
|
|
315
359
|
}
|
|
316
360
|
|
|
361
|
+
/** A colour picked by its value: the part takes it, and the asset's palette carries it in the same edit. */
|
|
362
|
+
private pickOwn(hex: string) {
|
|
363
|
+
const { token, colour } = ownColourOf(hex)
|
|
364
|
+
this.put({ token }, false, { [token]: colour })
|
|
365
|
+
}
|
|
366
|
+
|
|
317
367
|
/** A material change: recorded, or -- while a slider is dragged -- only drawn (`part-live`). */
|
|
318
|
-
private put(patch: Partial<FigureMaterial>, live = false) {
|
|
319
|
-
const edit = materialEditOf(this.body!.id, this.body!.material, patch, this.palette, this.offered())
|
|
368
|
+
private put(patch: Partial<FigureMaterial>, live = false, more: Record<string, string> = {}) {
|
|
369
|
+
const edit = materialEditOf(this.body!.id, this.body!.material, patch, this.palette, { ...this.offered(), ...more })
|
|
320
370
|
if (live) this.dispatchEvent(new CustomEvent('part-live', { detail: { edit }, bubbles: true, composed: true }))
|
|
321
371
|
else this.edit(edit)
|
|
322
372
|
}
|
|
@@ -48,6 +48,9 @@ const KIND_ICON: Record<string, string> = {
|
|
|
48
48
|
'hollow-box': 'check_box_outline_blank',
|
|
49
49
|
polygon: 'pentagon',
|
|
50
50
|
pyramid: 'details',
|
|
51
|
+
'tapered-box': 'change_history',
|
|
52
|
+
pipe: 'conversion_path',
|
|
53
|
+
duct: 'conversion_path',
|
|
51
54
|
portal: 'door_front'
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -58,7 +61,8 @@ const KIND_ICON: Record<string, string> = {
|
|
|
58
61
|
corners rounded or an inside made, from the part's own 단면 section (§3.3), so the palette does not offer a second way.
|
|
59
62
|
*/
|
|
60
63
|
/* A mesh part is made from a file (`import-mesh`), not from a starting size. */
|
|
61
|
-
|
|
64
|
+
/* A tapered box is a box with its top resized in the section editor, and a duct a pipe of a rectangle: no second way here. */
|
|
65
|
+
export type FigurePaletteKind = Exclude<FigurePrimitiveCreation['kind'], 'mesh' | 'tapered-box' | 'duct'> | 'plate' | 'wall'
|
|
62
66
|
const RECIPES: Array<{ kind: FigurePaletteKind; icon: string; label: string }> = [
|
|
63
67
|
{ kind: 'box', icon: 'deployed_code', label: 'figure.label.primitive-cube' },
|
|
64
68
|
{ kind: 'cylinder', icon: 'database', label: 'figure.label.primitive-cylinder' },
|
|
@@ -66,6 +70,7 @@ const RECIPES: Array<{ kind: FigurePaletteKind; icon: string; label: string }> =
|
|
|
66
70
|
{ kind: 'frustum', icon: 'change_history', label: 'figure.label.primitive-frustum' },
|
|
67
71
|
{ kind: 'polygon', icon: 'hexagon', label: 'figure.label.primitive-polygon' },
|
|
68
72
|
{ kind: 'pyramid', icon: 'details', label: 'figure.label.primitive-pyramid' },
|
|
73
|
+
{ kind: 'pipe', icon: 'conversion_path', label: 'figure.label.primitive-pipe' },
|
|
69
74
|
{ kind: 'plate', icon: 'check_box_outline_blank', label: 'figure.label.primitive-plate' },
|
|
70
75
|
{ kind: 'wall', icon: 'crop_portrait', label: 'figure.label.primitive-wall' },
|
|
71
76
|
{ kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }
|
|
@@ -605,7 +610,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
605
610
|
<option value="" ?selected=${!b.level}>${i18next.t('figure.label.detail-none')}</option>
|
|
606
611
|
${DETAIL_LEVELS.map(
|
|
607
612
|
one => html`<option value=${one} ?selected=${one === b.level}>
|
|
608
|
-
${one} ${i18next.t('figure.label.detail-' + one)} · ${i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}
|
|
613
|
+
${one === 'UNLIMITED' ? '' : `${one} `}${i18next.t('figure.label.detail-' + one)} · ${PART_LIMIT[one] === null ? i18next.t('figure.text.no-part-limit') : i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}
|
|
609
614
|
</option>`
|
|
610
615
|
)}
|
|
611
616
|
</select>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
3
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
4
|
+
import { live } from 'lit/directives/live.js'
|
|
5
|
+
|
|
6
|
+
import { i18next, localize } from '@operato/i18n'
|
|
7
|
+
import { FIGURE_SWEEP_PIECE_LIMIT, type FigureEvaluatedShape, type FigurePipeRequest } from '@hatiolab/figure-model'
|
|
8
|
+
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
9
|
+
import { bendWordOf, piecesOf, QUARTERS, type PanelPiece } from './pipe-path.js'
|
|
10
|
+
|
|
11
|
+
type Body = { id: string; shape: FigureEvaluatedShape }
|
|
12
|
+
/**
|
|
13
|
+
* 관 — a pipe's or a duct's section, wall and path (ADR-0100 §16.2), each change one `set-pipe` command, one step of the
|
|
14
|
+
* history. The path is its pieces in order: a straight run, so long; a bend, of a radius, so many degrees, toward one of
|
|
15
|
+
* four directions across the pipe -- each said as the figure's axis it turns to there. What the kernel refuses (a bend
|
|
16
|
+
* tighter than the section, a wall too thick, a path through itself) is said by the editor, with the number it takes.
|
|
17
|
+
*/
|
|
18
|
+
@customElement('figure-pipe-panel')
|
|
19
|
+
export class FigurePipePanel extends localize(i18next)(LitElement) {
|
|
20
|
+
static styles = [
|
|
21
|
+
SidePanelStyles,
|
|
22
|
+
css`
|
|
23
|
+
:host {
|
|
24
|
+
display: block;
|
|
25
|
+
}
|
|
26
|
+
div[pipe] {
|
|
27
|
+
display: grid;
|
|
28
|
+
gap: 8px;
|
|
29
|
+
}
|
|
30
|
+
ol[pieces] {
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: 6px;
|
|
33
|
+
margin: 0;
|
|
34
|
+
padding: 0;
|
|
35
|
+
list-style: none;
|
|
36
|
+
}
|
|
37
|
+
ol[pieces] li {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: 20px minmax(0, 1fr) 30px;
|
|
40
|
+
gap: 4px 6px;
|
|
41
|
+
align-items: center;
|
|
42
|
+
padding: 6px 0;
|
|
43
|
+
border-top: 1px dashed var(--md-sys-color-outline-variant);
|
|
44
|
+
}
|
|
45
|
+
ol[pieces] li > b {
|
|
46
|
+
font-variant-numeric: tabular-nums;
|
|
47
|
+
opacity: 0.7;
|
|
48
|
+
}
|
|
49
|
+
ol[pieces] li div[form] {
|
|
50
|
+
grid-column: 2;
|
|
51
|
+
}
|
|
52
|
+
ol[pieces] li button[act] {
|
|
53
|
+
width: 30px;
|
|
54
|
+
min-height: var(--figure-control-height);
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
}
|
|
58
|
+
div[pair] {
|
|
59
|
+
display: flex;
|
|
60
|
+
gap: 6px;
|
|
61
|
+
}
|
|
62
|
+
div[pair] > button {
|
|
63
|
+
flex: 1;
|
|
64
|
+
}
|
|
65
|
+
md-icon {
|
|
66
|
+
font-size: 16px;
|
|
67
|
+
width: 16px;
|
|
68
|
+
height: 16px;
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
@property({ attribute: false }) body?: Body
|
|
74
|
+
@property({ type: Boolean }) locked = false
|
|
75
|
+
|
|
76
|
+
private send(request: FigurePipeRequest) {
|
|
77
|
+
this.dispatchEvent(new CustomEvent('command', { detail: { action: { kind: 'set-pipe', part: this.body!.id, ...request } }, bubbles: true, composed: true }))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A number field: sent on change, put back when it is not a number. */
|
|
81
|
+
private number(label: string, now: number, set: (value: number) => void, options: { min?: number; step?: string; note?: string } = {}) {
|
|
82
|
+
return html`<span>${label}</span>
|
|
83
|
+
<div>
|
|
84
|
+
<input
|
|
85
|
+
type="number"
|
|
86
|
+
step=${options.step ?? 'any'}
|
|
87
|
+
min=${options.min ?? 0}
|
|
88
|
+
aria-label=${label}
|
|
89
|
+
?disabled=${this.locked}
|
|
90
|
+
.value=${live(String(Math.round(now * 1000) / 1000))}
|
|
91
|
+
@change=${(e: Event) => {
|
|
92
|
+
const input = e.target as HTMLInputElement
|
|
93
|
+
const value = input.valueAsNumber
|
|
94
|
+
if (!Number.isFinite(value)) {
|
|
95
|
+
input.value = String(now)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
set(value)
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
${options.note ? html`<small>${options.note}</small>` : nothing}
|
|
102
|
+
</div>`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Its end's size: the same as its start's until changed, when it narrows or widens to it evenly along its length
|
|
107
|
+
* (ADR-0100 §16.2.9) -- a bar thick below and thin above. 「한 굵기로」 makes it one size all along again.
|
|
108
|
+
*/
|
|
109
|
+
private renderEnd(sweep: NonNullable<FigureEvaluatedShape['sweep']>, round: boolean) {
|
|
110
|
+
const s = sweep.section,
|
|
111
|
+
to = sweep.to
|
|
112
|
+
const same = html`<span></span>
|
|
113
|
+
<div>
|
|
114
|
+
<button act end-same ?disabled=${this.locked} @click=${() => this.send(round ? { endRadius: null } : { endWidth: null, endDepth: null })}>
|
|
115
|
+
${i18next.t('figure.button.pipe-end-same')}
|
|
116
|
+
</button>
|
|
117
|
+
</div>`
|
|
118
|
+
if (round) {
|
|
119
|
+
const now = to?.kind === 'ellipse' ? 2 * to.radius.x! : s.kind === 'ellipse' ? 2 * s.radius.x! : 0
|
|
120
|
+
return html`${this.number(i18next.t('figure.label.pipe-end-diameter'), now, value => this.send({ endRadius: value / 2 }), { note: to ? '' : i18next.t('figure.text.pipe-end-same') })}
|
|
121
|
+
${to ? same : nothing}`
|
|
122
|
+
}
|
|
123
|
+
if (s.kind !== 'rectangle') return nothing
|
|
124
|
+
const end = to?.kind === 'rectangle' ? to.size : s.size
|
|
125
|
+
return html`${this.number(i18next.t('figure.label.pipe-end-width'), end.x!, value => this.send({ endWidth: value }), { note: to ? '' : i18next.t('figure.text.pipe-end-same') })}
|
|
126
|
+
${this.number(i18next.t('figure.label.pipe-end-depth'), end.z!, value => this.send({ endDepth: value }))}
|
|
127
|
+
${to ? same : nothing}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
render() {
|
|
131
|
+
const sweep = this.body?.shape.sweep
|
|
132
|
+
if (!sweep) return nothing
|
|
133
|
+
const s = sweep.section
|
|
134
|
+
const round = s.kind === 'ellipse'
|
|
135
|
+
const pieces = piecesOf(sweep)
|
|
136
|
+
const withPiece = (i: number, piece: PanelPiece) => this.send({ path: pieces.map((p, j) => (j === i ? piece : p)) })
|
|
137
|
+
const radius = round ? s.radius.x ?? s.radius.z ?? 0 : 0
|
|
138
|
+
/* The group's heading is the part's properties' own, as a box's size group's is: here only what is in it. */
|
|
139
|
+
return html`<div pipe>
|
|
140
|
+
<div form>
|
|
141
|
+
<span>${i18next.t('figure.label.pipe-section')}</span>
|
|
142
|
+
<select
|
|
143
|
+
aria-label=${i18next.t('figure.label.pipe-section')}
|
|
144
|
+
?disabled=${this.locked}
|
|
145
|
+
@change=${(e: Event) => {
|
|
146
|
+
const to = (e.target as HTMLSelectElement).value
|
|
147
|
+
if (to === 'round' && !round) this.send({ radius: Math.min(s.kind === 'rectangle' ? s.size.x! : 0, s.kind === 'rectangle' ? s.size.z! : 0) / 2 })
|
|
148
|
+
if (to === 'square' && round) this.send({ width: 2 * radius, depth: 2 * radius, round: 0 })
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<option value="round" ?selected=${round}>${i18next.t('figure.label.pipe-section-round')}</option>
|
|
152
|
+
<option value="square" ?selected=${!round}>${i18next.t('figure.label.pipe-section-square')}</option>
|
|
153
|
+
</select>
|
|
154
|
+
${round
|
|
155
|
+
? this.number(i18next.t('figure.label.pipe-diameter'), 2 * radius, value => this.send({ radius: value / 2 }))
|
|
156
|
+
: s.kind === 'rectangle'
|
|
157
|
+
? html`${this.number(i18next.t('figure.label.pipe-width'), s.size.x!, value => this.send({ width: value }))}
|
|
158
|
+
${this.number(i18next.t('figure.label.pipe-depth'), s.size.z!, value => this.send({ depth: value }))}
|
|
159
|
+
${this.number(i18next.t('figure.label.pipe-round'), s.corner?.x ?? 0, value => this.send({ round: value }))}`
|
|
160
|
+
: nothing}
|
|
161
|
+
${this.renderEnd(sweep, round)}
|
|
162
|
+
${this.number(i18next.t('figure.label.pipe-wall'), sweep.wall ?? 0, value => this.send({ wall: value }), { note: i18next.t('figure.text.pipe-wall-zero') })}
|
|
163
|
+
</div>
|
|
164
|
+
<ol pieces aria-label=${i18next.t('figure.label.pipe-path')}>
|
|
165
|
+
${pieces.map(
|
|
166
|
+
(p, i) => html`<li>
|
|
167
|
+
<b>${i + 1}</b>
|
|
168
|
+
<div form>
|
|
169
|
+
${'line' in p
|
|
170
|
+
? this.number(i18next.t('figure.label.pipe-line-length'), p.line, value => withPiece(i, { line: value }))
|
|
171
|
+
: html`${this.number(i18next.t('figure.label.pipe-bend-radius'), p.bend.radius, value => withPiece(i, { bend: { ...p.bend, radius: value } }))}
|
|
172
|
+
${this.number(i18next.t('figure.label.pipe-bend-angle'), p.bend.angle, value => withPiece(i, { bend: { ...p.bend, angle: Math.round(value) } }), { min: 1, step: '1' })}
|
|
173
|
+
<span>${i18next.t('figure.label.pipe-bend-toward')}</span>
|
|
174
|
+
<select
|
|
175
|
+
aria-label=${i18next.t('figure.label.pipe-bend-toward')}
|
|
176
|
+
?disabled=${this.locked}
|
|
177
|
+
@change=${(e: Event) => withPiece(i, { bend: { ...p.bend, toward: Number((e.target as HTMLSelectElement).value) } })}
|
|
178
|
+
>
|
|
179
|
+
${[...new Set([...QUARTERS, p.bend.toward])].map(
|
|
180
|
+
t => html`<option value=${t} ?selected=${t === p.bend.toward}>${bendWordOf(sweep, i, t) || `${t}°`}</option>`
|
|
181
|
+
)}
|
|
182
|
+
</select>`}
|
|
183
|
+
</div>
|
|
184
|
+
<button
|
|
185
|
+
act
|
|
186
|
+
?disabled=${this.locked || pieces.length <= 1}
|
|
187
|
+
title=${i18next.t('figure.button.pipe-remove-piece')}
|
|
188
|
+
aria-label=${i18next.t('figure.button.pipe-remove-piece')}
|
|
189
|
+
@click=${() => this.send({ path: pieces.filter((_, j) => j !== i) })}
|
|
190
|
+
>
|
|
191
|
+
<md-icon>close</md-icon>
|
|
192
|
+
</button>
|
|
193
|
+
</li>`
|
|
194
|
+
)}
|
|
195
|
+
</ol>
|
|
196
|
+
<div pair>
|
|
197
|
+
<button act add-line ?disabled=${this.locked || pieces.length >= FIGURE_SWEEP_PIECE_LIMIT} @click=${() => this.send({ path: [...pieces, { line: 100 }] })}>
|
|
198
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.pipe-add-line')}
|
|
199
|
+
</button>
|
|
200
|
+
<button
|
|
201
|
+
act
|
|
202
|
+
add-bend
|
|
203
|
+
?disabled=${this.locked || pieces.length >= FIGURE_SWEEP_PIECE_LIMIT}
|
|
204
|
+
@click=${() => {
|
|
205
|
+
/* A bend wider than the section, toward its first quarter: 90° of twice the section's reach. */
|
|
206
|
+
const reach = round ? radius : Math.hypot(s.kind === 'rectangle' ? s.size.x! : 0, s.kind === 'rectangle' ? s.size.z! : 0) / 2
|
|
207
|
+
this.send({ path: [...pieces, { bend: { radius: Math.ceil(2 * reach), angle: 90, toward: 0 } }] })
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.pipe-add-bend')}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>`
|
|
214
|
+
}
|
|
215
|
+
}
|