@things-factory/figure-ui 10.1.271 → 10.1.273
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/canvas.ts +4 -2
- package/client/modeller/clips.ts +74 -4
- package/client/modeller/data-interface.ts +30 -18
- package/client/modeller/editor-styles.ts +17 -6
- package/client/modeller/editor.ts +2 -0
- package/client/modeller/fasten-form.ts +81 -6
- package/client/modeller/fasten-geometry.ts +5 -3
- package/client/modeller/figure-cavity-drawing.ts +1 -3
- package/client/modeller/figure-clips.ts +101 -8
- package/client/modeller/figure-data-panel.ts +384 -200
- package/client/modeller/figure-fasten-dialog.ts +41 -30
- package/client/modeller/figure-fasten-drawing.ts +24 -14
- package/client/modeller/figure-first-save.ts +4 -3
- package/client/modeller/figure-links.ts +109 -37
- package/client/modeller/figure-parts.ts +7 -11
- package/client/modeller/figure-preview.ts +299 -79
- package/client/modeller/figure-size-editor.ts +7 -2
- package/client/modeller/first-save.ts +8 -0
- package/client/modeller/focus-on-render.ts +4 -0
- package/client/modeller/pivot.ts +76 -0
- package/client/modeller/size-response.ts +14 -0
- package/client/modeller/stage-model.ts +50 -6
- package/dist-client/modeller/canvas.d.ts +1 -0
- package/dist-client/modeller/canvas.js +3 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +17 -3
- package/dist-client/modeller/clips.js +69 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +11 -8
- package/dist-client/modeller/data-interface.js +27 -17
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +17 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.js +2 -0
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +13 -0
- package/dist-client/modeller/fasten-form.js +72 -7
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +5 -3
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.js +1 -3
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +104 -8
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +10 -19
- package/dist-client/modeller/figure-data-panel.js +382 -190
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +39 -24
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +24 -14
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.js +4 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -0
- package/dist-client/modeller/figure-links.js +120 -51
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +7 -11
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +13 -0
- package/dist-client/modeller/figure-preview.js +296 -80
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.js +7 -3
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +5 -0
- package/dist-client/modeller/first-save.js +7 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/focus-on-render.d.ts +2 -0
- package/dist-client/modeller/focus-on-render.js +4 -0
- package/dist-client/modeller/focus-on-render.js.map +1 -0
- package/dist-client/modeller/pivot.d.ts +12 -0
- package/dist-client/modeller/pivot.js +59 -0
- package/dist-client/modeller/pivot.js.map +1 -0
- package/dist-client/modeller/size-response.d.ts +6 -0
- package/dist-client/modeller/size-response.js +14 -0
- package/dist-client/modeller/size-response.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +17 -3
- package/dist-client/modeller/stage-model.js +44 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +6 -5
- package/test/browser/links.check.mjs +30 -0
- package/test/browser/modeller.check.mjs +2 -2
- package/test/browser/parts-page.js +4 -0
- package/test/browser/parts.check.mjs +16 -10
- package/test/browser/screen-page.js +12 -1
- package/test/browser/screen.check.mjs +142 -11
- package/test/browser/section.check.mjs +21 -0
- package/test/browser/serve-response.mjs +5 -2
- package/test/browser/side-panel-sweep.check.mjs +171 -10
- package/test/clips.test.ts +49 -4
- package/test/data-motion.test.ts +39 -8
- package/test/fasten-form.test.ts +69 -0
- package/test/fasten-geometry.test.ts +13 -0
- package/test/first-save.test.ts +11 -1
- package/test/pivot.test.ts +57 -0
- package/test/size-response.test.ts +10 -2
- package/test/stage-model.test.ts +23 -1
- package/translations/en.json +31 -11
- package/translations/es.json +30 -10
- package/translations/ja.json +30 -10
- package/translations/ko.json +32 -12
- package/translations/ms.json +30 -10
- package/translations/zh.json +30 -10
- package/tutorial/humanoid/README.md +22 -0
- package/tutorial/humanoid/chapters.mjs +247 -0
- package/tutorial/humanoid/encode.mjs +26 -0
- package/tutorial/humanoid/lib.mjs +197 -0
- package/tutorial/humanoid/record.mjs +65 -0
|
@@ -89,7 +89,28 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
89
89
|
}
|
|
90
90
|
details[fasten-detail] > div[form] {
|
|
91
91
|
margin-top: 8px;
|
|
92
|
-
max-width:
|
|
92
|
+
max-width: 420px;
|
|
93
|
+
}
|
|
94
|
+
/* Where across the face, as buttons: a list the system draws is not seen in a recording, nor reached by a key in every browser. */
|
|
95
|
+
div[align-choices] {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
gap: 4px;
|
|
99
|
+
}
|
|
100
|
+
div[align-choices] button {
|
|
101
|
+
font: inherit;
|
|
102
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
103
|
+
padding: 3px 8px;
|
|
104
|
+
border-radius: 12px;
|
|
105
|
+
border: 1px solid var(--md-sys-color-outline-variant, #c4c7c5);
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: inherit;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
}
|
|
110
|
+
div[align-choices] button[aria-pressed='true'] {
|
|
111
|
+
border-color: var(--md-sys-color-primary, #0d8f86);
|
|
112
|
+
background: var(--md-sys-color-primary-container, #cdeeea);
|
|
113
|
+
font-weight: 600;
|
|
93
114
|
}
|
|
94
115
|
span[fasten-summary] {
|
|
95
116
|
opacity: 0.8;
|
|
@@ -253,7 +274,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
253
274
|
<summary>
|
|
254
275
|
${i18next.t('figure.label.fasten-detail')}${detail.length ? html` · <span fasten-summary>${detail.join(' · ')}</span>` : nothing}
|
|
255
276
|
</summary>
|
|
256
|
-
${plan.alignRows.length ? this.renderAlignment(plan.alignRows) : nothing}
|
|
277
|
+
${(plan.alignAxes ?? plan.alignRows).length ? this.renderAlignment(plan.alignAxes ?? plan.alignRows) : nothing}
|
|
257
278
|
<div form>
|
|
258
279
|
<span>${i18next.t('figure.label.gap')}</span>
|
|
259
280
|
<input
|
|
@@ -384,34 +405,24 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
384
405
|
: 'centre'
|
|
385
406
|
: (f.align[axis] ?? 'centre')
|
|
386
407
|
return html`<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
edges
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
407
|
-
${
|
|
408
|
-
this.revision && hasInstanceSize(this.revision.asset, axis)
|
|
409
|
-
? html`<option value="share" ?selected=${how === 'share'}>
|
|
410
|
-
${i18next.t('figure.label.align-share')}
|
|
411
|
-
</option>`
|
|
412
|
-
: nothing
|
|
413
|
-
}
|
|
414
|
-
</select>
|
|
408
|
+
<div align-choices role="group" aria-label=${i18next.t('figure.label.align-' + axis)}>
|
|
409
|
+
${[
|
|
410
|
+
{ value: 'centre' as Alignment, words: i18next.t('figure.label.align-centre'), shown: true },
|
|
411
|
+
{ value: 'min' as Alignment, words: i18next.t(`figure.label.align-${axis}-min`), shown: edges },
|
|
412
|
+
{ value: 'max' as Alignment, words: i18next.t(`figure.label.align-${axis}-max`), shown: edges },
|
|
413
|
+
{
|
|
414
|
+
value: 'input' as Alignment,
|
|
415
|
+
words: f.alignInput?.[axis] ? i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis]!, key => i18next.t(key)) }) : '',
|
|
416
|
+
shown: !!f.alignInput?.[axis]
|
|
417
|
+
},
|
|
418
|
+
{ value: 'mm' as Alignment, words: i18next.t('figure.label.align-mm'), shown: true },
|
|
419
|
+
{ value: 'share' as Alignment, words: i18next.t('figure.label.align-share'), shown: !!this.revision && hasInstanceSize(this.revision.asset, axis) }
|
|
420
|
+
]
|
|
421
|
+
.filter(choice => choice.shown)
|
|
422
|
+
.map(
|
|
423
|
+
choice => html`<button type="button" aria-pressed=${String(how === choice.value)} @click=${() => set(axis, choice.value)}>${choice.words}</button>`
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
415
426
|
${
|
|
416
427
|
how === 'mm' || how === 'share'
|
|
417
428
|
? html`<span></span
|
|
@@ -6,6 +6,7 @@ import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-
|
|
|
6
6
|
import { figureAroundNormalOf, figureAroundOf, figureCaughtAngleOf, figureSurfaceAnglesOf, figureSurfacePlaceOf } from '@hatiolab/figure-model'
|
|
7
7
|
import type { FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
8
8
|
|
|
9
|
+
import { focusOnRender } from './focus-on-render.js'
|
|
9
10
|
import { alignmentAt, drawingSpanOf, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
10
11
|
import type { Alignment } from './links.js'
|
|
11
12
|
|
|
@@ -72,22 +73,28 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
72
73
|
svg text {
|
|
73
74
|
font: inherit;
|
|
74
75
|
}
|
|
76
|
+
/*
|
|
77
|
+
The ways on one line, a small side view beside each word: three cards of a drawing over a word made the window
|
|
78
|
+
tall, and the fasten button below went off a laptop's screen (user 2026-11-12). The chosen one's gap is dragged
|
|
79
|
+
on its small view still; the gap's field is in the details.
|
|
80
|
+
*/
|
|
75
81
|
div[modes] {
|
|
76
|
-
display:
|
|
77
|
-
|
|
78
|
-
gap:
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
gap: 4px;
|
|
79
85
|
}
|
|
80
86
|
div[modes] svg {
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
flex: none;
|
|
88
|
+
width: 28px;
|
|
89
|
+
height: 20px;
|
|
83
90
|
}
|
|
84
91
|
/* A choice drawn, not a box to press (the side's rule: no boxed buttons): the chosen one on the primary's ground. */
|
|
85
92
|
div[modes] button {
|
|
86
|
-
display:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 4px;
|
|
96
|
+
padding: 2px 8px 2px 4px;
|
|
97
|
+
white-space: nowrap;
|
|
91
98
|
font: inherit;
|
|
92
99
|
color: var(--md-sys-color-on-surface);
|
|
93
100
|
background: none;
|
|
@@ -122,8 +129,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
122
129
|
border: 1px solid var(--md-sys-color-primary);
|
|
123
130
|
}
|
|
124
131
|
div[modes] button[on] {
|
|
125
|
-
background: var(--md-sys-color-primary-container);
|
|
126
|
-
color: var(--md-sys-color-on-primary-container);
|
|
132
|
+
background: var(--md-sys-color-primary-container, #d8e2ff);
|
|
133
|
+
color: var(--md-sys-color-on-primary-container, #001a41);
|
|
127
134
|
}
|
|
128
135
|
/* The box and the face seen from above side by side, one under the other where the window is narrow. */
|
|
129
136
|
div[face-row] {
|
|
@@ -332,7 +339,9 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
332
339
|
const { h, v, sign } = screenOf(view.axes)
|
|
333
340
|
const W = WHERE_W,
|
|
334
341
|
H = WHERE_H
|
|
335
|
-
|
|
342
|
+
/* Where the part is set, not where it is being dragged: the drawing's scale holds still under the pointer. */
|
|
343
|
+
const set = (a: Axis) => offsetOf(this.align[a], this.alignMm[a], view.face[a], view.part[a])
|
|
344
|
+
const span = (a: Axis) => drawingSpanOf(view.face[a], view.part[a], set(a))
|
|
336
345
|
const k = Math.min((W - 40) / span(h), (H - 40) / span(v))
|
|
337
346
|
this.scale = k
|
|
338
347
|
const cx = W / 2,
|
|
@@ -432,7 +441,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
432
441
|
this.change({ axis: a, how: 'mm', mm: Math.round(value) })
|
|
433
442
|
this.released()
|
|
434
443
|
}}
|
|
435
|
-
@blur=${() => (this.typing = undefined)}
|
|
444
|
+
@blur=${() => (this.typing = undefined)}
|
|
445
|
+
${focusOnRender()} />`
|
|
436
446
|
}
|
|
437
447
|
|
|
438
448
|
private grab(ev: PointerEvent, at: Record<Axis, number>, caught: Partial<Record<Axis, string>>) {
|
|
@@ -7,7 +7,7 @@ import { CommonPopupStyles, FormFieldStyles } from '@operato/styles'
|
|
|
7
7
|
import '@material/web/button/filled-button.js'
|
|
8
8
|
import '@material/web/button/text-button.js'
|
|
9
9
|
|
|
10
|
-
import { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, type FirstSave } from './first-save.js'
|
|
10
|
+
import { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, typingTypeOf, type FirstSave } from './first-save.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The first save of a new figure: its type and its name, asked once, in the suite's popup look.
|
|
@@ -97,13 +97,13 @@ export class FigureFirstSave extends LitElement {
|
|
|
97
97
|
.value=${this.type}
|
|
98
98
|
placeholder=${i18next.t('figure.text.figure-type-example')}
|
|
99
99
|
@input=${(e: Event) => {
|
|
100
|
-
this.type =
|
|
100
|
+
this.type = typingTypeOf((e.target as HTMLInputElement).value)
|
|
101
101
|
;(e.target as HTMLInputElement).value = this.type
|
|
102
102
|
this.refusal = null
|
|
103
103
|
}}
|
|
104
104
|
@keydown=${this.onKey}
|
|
105
105
|
/>
|
|
106
|
-
${this.type && !releasableType(this.type)
|
|
106
|
+
${typeOf(this.type) && !releasableType(typeOf(this.type))
|
|
107
107
|
? html`<div class="note">${i18next.t('figure.text.type-name-will-not-release')}</div>`
|
|
108
108
|
: this.suggested && this.type === this.suggested
|
|
109
109
|
? html`<div class="note">${i18next.t('figure.text.identifier-suggested')}</div>`
|
|
@@ -132,6 +132,7 @@ export class FigureFirstSave extends LitElement {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
private confirm = () => {
|
|
135
|
+
this.type = typeOf(this.type)
|
|
135
136
|
this.refusal = firstSaveRefusalOf(this.type, this.taken)
|
|
136
137
|
if (this.refusal) return
|
|
137
138
|
const namespace = this.namespaces.length > 1 ? this.chosenNamespace : undefined
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
stateSizeFixedOf,
|
|
51
51
|
type StateSizeForm
|
|
52
52
|
} from './links.js'
|
|
53
|
-
import { measureFactor } from './size-response.js'
|
|
53
|
+
import { measureFactor, sizeAxisWord } from './size-response.js'
|
|
54
54
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
55
55
|
import { insideAlignmentOf } from './fasten-geometry.js'
|
|
56
56
|
import { fastenFormOf, fastenPlanOf, fastenSentenceOf } from './fasten-form.js'
|
|
@@ -58,9 +58,22 @@ import type { FigureRevision } from './revision.js'
|
|
|
58
58
|
import { HelpToggles } from './help.js'
|
|
59
59
|
import { groupMenu, GroupMenuStyles } from './group-menu.js'
|
|
60
60
|
import { jointAxisWord } from './joint-axis.js'
|
|
61
|
+
import { PIVOT_POINTS, pivotPointOf, pivotPointsOf, type PivotPoint } from './pivot.js'
|
|
61
62
|
import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js'
|
|
62
63
|
|
|
63
64
|
const freshMotion = (): MotionForm => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 })
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The motion a part's form starts from: a part hung on another part most often turns there -- a thigh at the hip, a door
|
|
68
|
+
* at its frame -- so it starts as a turn; a part on the plane, or on nothing, as a slide (review 2026-11-11: eight
|
|
69
|
+
* joints of a humanoid, each changed from slide to turn by hand).
|
|
70
|
+
*/
|
|
71
|
+
export function motionStartOf(revision: FigureRevision | undefined, part: string): MotionForm {
|
|
72
|
+
const own = revision ? fasteningOf(revision, part) : undefined
|
|
73
|
+
return own && !own.toPlane && own.target ? { ...freshMotion(), kind: 'turn', min: -90, max: 90 } : freshMotion()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sameMotion = (a: MotionForm, b: MotionForm) => JSON.stringify(a) === JSON.stringify(b)
|
|
64
77
|
const freshRow = () => ({ axis: '' as Axis | '', pitch: 200, fills: true, count: 5 })
|
|
65
78
|
const freshPlace = () => ({ role: 'grip' as 'seat' | 'grip', face: 'top' as FigureFace, label: '', accepts: '', capacity: 1 })
|
|
66
79
|
|
|
@@ -82,9 +95,31 @@ const freshPlace = () => ({ role: 'grip' as 'seat' | 'grip', face: 'top' as Figu
|
|
|
82
95
|
export class FigureLinks extends localize(i18next)(LitElement) {
|
|
83
96
|
private readonly help = new HelpToggles(this)
|
|
84
97
|
static styles = [SidePanelStyles, GroupMenuStyles, css`
|
|
98
|
+
/* A motion's delete and edit as icons at the line's end, their words in their titles: two word buttons took the line (user 2026-11-12). */
|
|
99
|
+
div[pair] > button[act][icon] {
|
|
100
|
+
flex: none;
|
|
101
|
+
width: var(--figure-control-height, 28px);
|
|
102
|
+
padding: 0;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
}
|
|
105
|
+
/* The size-by-state form folded under its summary, opened to add one. */
|
|
106
|
+
details[state-size-add][open] > summary {
|
|
107
|
+
margin-bottom: 6px;
|
|
108
|
+
}
|
|
85
109
|
:host {
|
|
86
110
|
display: block;
|
|
87
111
|
}
|
|
112
|
+
/* The turn's axis through the part's top, middle or bottom, by name, under the numbers it fills in. */
|
|
113
|
+
div[pivot-points] {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 8px;
|
|
117
|
+
margin: 4px 0 8px;
|
|
118
|
+
}
|
|
119
|
+
div[pivot-points] > span {
|
|
120
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
121
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
122
|
+
}
|
|
88
123
|
/* A group's sub-question, under its title: quieter than the title, as a label is. */
|
|
89
124
|
p[group-question] {
|
|
90
125
|
margin: 4px 0 0;
|
|
@@ -216,7 +251,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
216
251
|
if (changed.has('asset') || changed.has('partId')) this.fixNote = ''
|
|
217
252
|
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
218
253
|
this.loadedFor = this.partId
|
|
219
|
-
this.motion =
|
|
254
|
+
this.motion = motionStartOf(this.revision, this.partId)
|
|
220
255
|
this.place = freshPlace()
|
|
221
256
|
this.placeEditing = null
|
|
222
257
|
this.row = freshRow()
|
|
@@ -224,6 +259,10 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
224
259
|
this.rowCandidate = undefined
|
|
225
260
|
this.editingMotion = false
|
|
226
261
|
this.motionAxis = 0
|
|
262
|
+
} else if (changed.has('revision') && this.partId && !this.editingMotion) {
|
|
263
|
+
/* Fastened since the form was loaded, a form nobody has touched starts as that part's motion now does. */
|
|
264
|
+
const was = motionStartOf(changed.get('revision') as FigureRevision | undefined, this.partId)
|
|
265
|
+
if (sameMotion(this.motion, was)) this.motion = motionStartOf(this.revision, this.partId)
|
|
227
266
|
}
|
|
228
267
|
}
|
|
229
268
|
|
|
@@ -624,7 +663,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
624
663
|
const part = this.partId
|
|
625
664
|
const axes = followableAxes(asset, part)
|
|
626
665
|
if (!axes.length) return nothing
|
|
627
|
-
const word = (axis: Axis) =>
|
|
666
|
+
const word = (axis: Axis) => sizeAxisWord(asset, part, axis, i18next.t('figure.label.size-diameter'))
|
|
628
667
|
const sized = axes.flatMap(axis => {
|
|
629
668
|
const f = followOf(asset, part, axis)
|
|
630
669
|
return f.of === 'state' ? [{ axis, f }] : []
|
|
@@ -645,7 +684,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
645
684
|
: { ...form, id: value }
|
|
646
685
|
}
|
|
647
686
|
const number = (key: 'min' | 'max' | 'atMin' | 'atMax') => (e: Event) => (this.stateSize = { ...form, axis, [key]: (e.target as HTMLInputElement).valueAsNumber })
|
|
648
|
-
return html`<h4 state-size>${i18next.t('figure.label.state-size')}</h4
|
|
687
|
+
return html`${sized.length ? html`<h4 state-size>${i18next.t('figure.label.state-size')}</h4>` : nothing}
|
|
649
688
|
${sized.map(({ axis: one, f }) => {
|
|
650
689
|
const k = measureFactor(asset, part, one)
|
|
651
690
|
const input = controls.find(c => c.id === f.input)
|
|
@@ -667,7 +706,10 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
667
706
|
</div>`
|
|
668
707
|
})}
|
|
669
708
|
${free.length
|
|
670
|
-
?
|
|
709
|
+
? /* Folded until asked for: six lines of a form few parts use pushed the controls below it down (user 2026-11-12). */
|
|
710
|
+
html`<details state-size-add>
|
|
711
|
+
<summary>${i18next.t('figure.button.add-state-size')}</summary>
|
|
712
|
+
<div form state-size-form>
|
|
671
713
|
<span>${i18next.t('figure.label.state-size-measure')}</span>
|
|
672
714
|
<select ?disabled=${this.locked} @change=${(e: Event) => (this.stateSize = { ...form, axis: (e.target as HTMLSelectElement).value as Axis, atMax: NaN })}>
|
|
673
715
|
${free.map(one => html`<option value=${one} ?selected=${one === axis}>${word(one)}</option>`)}
|
|
@@ -696,7 +738,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
696
738
|
if (!command) return
|
|
697
739
|
this.command(command)
|
|
698
740
|
this.stateSize = freshStateSize()
|
|
699
|
-
}}><md-icon>
|
|
741
|
+
}}><md-icon>add</md-icon>${i18next.t('figure.button.add')}</button>
|
|
742
|
+
</details>`
|
|
700
743
|
: nothing}`
|
|
701
744
|
}
|
|
702
745
|
|
|
@@ -760,7 +803,33 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
760
803
|
</div>`
|
|
761
804
|
)}
|
|
762
805
|
</div>
|
|
763
|
-
</div
|
|
806
|
+
</div>
|
|
807
|
+
${this.renderPivotPoints(m)}`
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/*
|
|
811
|
+
The axis put through the part's top end, middle or bottom end by name: the numbers above are measured from the
|
|
812
|
+
joint's origin, which a person cannot see (tutorial run 2026-11-12: a shoulder given Y 120 "to reach the shoulder"
|
|
813
|
+
turned about a point above it). The button that matches what is typed stands pressed.
|
|
814
|
+
*/
|
|
815
|
+
private pivotCache?: { key: string; points: ReturnType<typeof pivotPointsOf> }
|
|
816
|
+
private renderPivotPoints(m: MotionForm) {
|
|
817
|
+
if (!this.revision || m.throughFollows) return nothing
|
|
818
|
+
const key = `${this.revision.revision}:${this.partId}:${m.axis}`
|
|
819
|
+
if (this.pivotCache?.key !== key) this.pivotCache = { key, points: pivotPointsOf(this.revision, this.partId, m.axis as Axis) }
|
|
820
|
+
const points = this.pivotCache.points
|
|
821
|
+
if (!points) return nothing
|
|
822
|
+
return html`<div pivot-points>
|
|
823
|
+
<span>${i18next.t('figure.label.pivot-at')}</span>
|
|
824
|
+
<ox-input-select-buttons
|
|
825
|
+
.value=${pivotPointOf(points, m.through, m.axis as Axis) ?? ''}
|
|
826
|
+
.options=${PIVOT_POINTS.map(p => ({ value: p, display: i18next.t(`figure.label.pivot-${p}`) }))}
|
|
827
|
+
@change=${(e: CustomEvent) => {
|
|
828
|
+
const at = points[e.detail as PivotPoint]
|
|
829
|
+
if (at) this.motion = { ...m, through: { ...at } }
|
|
830
|
+
}}
|
|
831
|
+
></ox-input-select-buttons>
|
|
832
|
+
</div>`
|
|
764
833
|
}
|
|
765
834
|
|
|
766
835
|
private renderMotion(asset: FigureAsset) {
|
|
@@ -796,14 +865,14 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
796
865
|
: nothing}
|
|
797
866
|
</span>
|
|
798
867
|
${count > 1
|
|
799
|
-
? html`<button act remove-axis ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: withoutAxis(joint, k) })}>
|
|
800
|
-
<md-icon>
|
|
868
|
+
? html`<button act icon remove-axis title=${i18next.t('figure.button.remove-axis')} aria-label=${i18next.t('figure.button.remove-axis')} ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: withoutAxis(joint, k) })}>
|
|
869
|
+
<md-icon>delete</md-icon>
|
|
801
870
|
</button>`
|
|
802
|
-
: html`<button act ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
|
|
803
|
-
<md-icon>
|
|
871
|
+
: html`<button act icon title=${i18next.t('figure.button.remove-motion')} aria-label=${i18next.t('figure.button.remove-motion')} ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
|
|
872
|
+
<md-icon>delete</md-icon>
|
|
804
873
|
</button>`}
|
|
805
|
-
<button act ?disabled=${this.locked || !joint} @click=${() => edit(k)}>
|
|
806
|
-
<md-icon>edit</md-icon
|
|
874
|
+
<button act icon title=${i18next.t('figure.button.change-motion')} aria-label=${i18next.t('figure.button.change-motion')} ?disabled=${this.locked || !joint} @click=${() => edit(k)}>
|
|
875
|
+
<md-icon>edit</md-icon>
|
|
807
876
|
</button>
|
|
808
877
|
</div>`
|
|
809
878
|
}
|
|
@@ -844,6 +913,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
844
913
|
const adding = k > 0 && k >= figureMotionsOf(joint).length
|
|
845
914
|
const follow = leaders.some(j => j.id === m.follow) ? m.follow : ''
|
|
846
915
|
const refusal = motionFormRefusalOf(m, follow)
|
|
916
|
+
/* Following another joint is offered after the part's own control: a row above it moved every field down a row, and
|
|
917
|
+
a name typed there chose a joint to follow (review 2026-11-11). */
|
|
847
918
|
return html`
|
|
848
919
|
${k > 0 ? html`<p motion-axis-heading><b>${i18next.t('figure.label.motion-axis-n', { n: k + 1 })}</b></p>` : nothing}
|
|
849
920
|
<div pair>
|
|
@@ -858,31 +929,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
858
929
|
${AXES.map(a => html`<option value=${a} ?selected=${a === m.axis}>${i18next.t('figure.label.motion-axis')} ${a.toUpperCase()}</option>`)}
|
|
859
930
|
</select>`}
|
|
860
931
|
</div>
|
|
861
|
-
${m.followSum
|
|
862
|
-
? html`<div form>
|
|
863
|
-
<span>${i18next.t('figure.label.motion-follow')}</span>
|
|
864
|
-
<span>${m.followSum.map(t => `${t.joint} × ${t.multiplier}`).join(' + ')}</span>
|
|
865
|
-
<span>${i18next.t('figure.label.motion-offset')} (${unit})</span>
|
|
866
|
-
<input type="number" step="any" .value=${String(m.offset)} @change=${(e: Event) => (this.motion = { ...m, offset: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
867
|
-
</div>`
|
|
868
|
-
: leaders.length
|
|
869
|
-
? html`<div form>
|
|
870
|
-
<span>${i18next.t('figure.label.motion-follow')}</span>
|
|
871
|
-
<select @change=${(e: Event) => (this.motion = { ...m, follow: (e.target as HTMLSelectElement).value })}>
|
|
872
|
-
<option value="" ?selected=${!follow}>${i18next.t('figure.label.motion-own-control')}</option>
|
|
873
|
-
${leaders.map(j => html`<option value=${j.id} ?selected=${j.id === follow}>${j.part}${j.state?.id ? ` · ${j.state.id}` : j.id !== j.part ? ` · ${j.id}` : ''}</option>`)}
|
|
874
|
-
</select>
|
|
875
|
-
</div>`
|
|
876
|
-
: nothing}
|
|
877
|
-
${m.followSum
|
|
932
|
+
${m.followSum || follow
|
|
878
933
|
? nothing
|
|
879
|
-
: follow
|
|
880
|
-
? html`<div form>
|
|
881
|
-
<span>${i18next.t('figure.label.motion-multiplier')}</span>
|
|
882
|
-
<input type="number" step="any" .value=${String(m.multiplier)} @change=${(e: Event) => (this.motion = { ...m, multiplier: Number((e.target as HTMLInputElement).value) })} />
|
|
883
|
-
<span>${i18next.t('figure.label.motion-offset')} (${unit})</span>
|
|
884
|
-
<input type="number" step="any" .value=${String(m.offset)} @change=${(e: Event) => (this.motion = { ...m, offset: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
885
|
-
</div>`
|
|
886
934
|
: html`<div form>
|
|
887
935
|
${m.kind === 'slide'
|
|
888
936
|
? html`<span>${i18next.t('figure.label.motion-travel')}</span>
|
|
@@ -910,6 +958,30 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
910
958
|
<input type="number" step="any" .value=${String(m.max)} @change=${(e: Event) => (this.motion = { ...m, max: Number((e.target as HTMLInputElement).value) })} />
|
|
911
959
|
</div>`}
|
|
912
960
|
</div>`}
|
|
961
|
+
${m.followSum
|
|
962
|
+
? html`<div form>
|
|
963
|
+
<span>${i18next.t('figure.label.motion-follow')}</span>
|
|
964
|
+
<span>${m.followSum.map(t => `${t.joint} × ${t.multiplier}`).join(' + ')}</span>
|
|
965
|
+
<span>${i18next.t('figure.label.motion-offset')} (${unit})</span>
|
|
966
|
+
<input type="number" step="any" .value=${String(m.offset)} @change=${(e: Event) => (this.motion = { ...m, offset: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
967
|
+
</div>`
|
|
968
|
+
: leaders.length
|
|
969
|
+
? html`<div form>
|
|
970
|
+
<span>${i18next.t('figure.label.motion-follow')}</span>
|
|
971
|
+
<select @change=${(e: Event) => (this.motion = { ...m, follow: (e.target as HTMLSelectElement).value })}>
|
|
972
|
+
<option value="" ?selected=${!follow}>${i18next.t('figure.label.motion-own-control')}</option>
|
|
973
|
+
${leaders.map(j => html`<option value=${j.id} ?selected=${j.id === follow}>${j.part}${j.state?.id ? ` · ${j.state.id}` : j.id !== j.part ? ` · ${j.id}` : ''}</option>`)}
|
|
974
|
+
</select>
|
|
975
|
+
</div>`
|
|
976
|
+
: nothing}
|
|
977
|
+
${!m.followSum && follow
|
|
978
|
+
? html`<div form>
|
|
979
|
+
<span>${i18next.t('figure.label.motion-multiplier')}</span>
|
|
980
|
+
<input type="number" step="any" .value=${String(m.multiplier)} @change=${(e: Event) => (this.motion = { ...m, multiplier: Number((e.target as HTMLInputElement).value) })} />
|
|
981
|
+
<span>${i18next.t('figure.label.motion-offset')} (${unit})</span>
|
|
982
|
+
<input type="number" step="any" .value=${String(m.offset)} @change=${(e: Event) => (this.motion = { ...m, offset: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
983
|
+
</div>`
|
|
984
|
+
: nothing}
|
|
913
985
|
${m.kind === 'slide' ? nothing : this.renderThrough(m)}
|
|
914
986
|
<button
|
|
915
987
|
act
|
|
@@ -444,9 +444,10 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
444
444
|
li[selected] div[ops] {
|
|
445
445
|
display: flex;
|
|
446
446
|
}
|
|
447
|
+
/* As tall as the eye beside the name: a chosen row is no taller than the others. */
|
|
447
448
|
div[ops] button {
|
|
448
449
|
display: flex;
|
|
449
|
-
padding:
|
|
450
|
+
padding: 1px;
|
|
450
451
|
color: inherit;
|
|
451
452
|
background: none;
|
|
452
453
|
border: none;
|
|
@@ -465,17 +466,12 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
465
466
|
color: inherit;
|
|
466
467
|
}
|
|
467
468
|
/*
|
|
468
|
-
|
|
469
|
-
|
|
469
|
+
The chosen row keeps the height of the others, however narrow the list: its buttons stay beside the name, which is
|
|
470
|
+
cut short (its whole name is in the title and the panel's head). Going to a line of their own under 200 px
|
|
471
|
+
(2026-09-24), they pushed every row under it down as a part was chosen (user 2026-11-12).
|
|
470
472
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
flex-wrap: wrap;
|
|
474
|
-
}
|
|
475
|
-
li[selected] div[ops] {
|
|
476
|
-
flex-basis: 100%;
|
|
477
|
-
justify-content: flex-end;
|
|
478
|
-
}
|
|
473
|
+
div[ops] {
|
|
474
|
+
flex: none;
|
|
479
475
|
}
|
|
480
476
|
p[quiet] {
|
|
481
477
|
margin: 0;
|