@things-factory/figure-ui 10.1.45 → 10.1.47
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/angle-dial.ts +70 -0
- package/client/modeller/figure-settings.ts +328 -0
- package/client/modeller/figure-v3-capability.ts +137 -0
- package/client/modeller/figure-v3-clips.ts +253 -0
- package/client/modeller/figure-v3-controls.ts +212 -0
- package/client/modeller/figure-v3-links.ts +150 -79
- package/client/modeller/figure-v3-material.ts +108 -35
- package/client/modeller/figure-v3-parts.ts +96 -3
- package/client/modeller/figure-v3-preview.ts +326 -0
- package/client/modeller/figure-v3-properties.ts +29 -57
- package/client/modeller/figure-v3-verdict.ts +598 -60
- package/client/modeller/figure-view.ts +152 -0
- package/client/modeller/side-panel-styles.ts +326 -0
- package/client/modeller/v3-canvas.ts +221 -263
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +532 -223
- package/client/modeller/v3-fields.ts +62 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +126 -10
- package/client/modeller/v3-live.ts +79 -0
- package/client/modeller/v3-maturity.ts +415 -0
- package/client/modeller/v3-palette.ts +45 -0
- package/client/modeller/v3-preview-model.ts +54 -0
- package/client/modeller/v3-stage-model.ts +159 -0
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/figure-v3-modeller-page.ts +66 -134
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +317 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
- package/dist-client/modeller/figure-v3-capability.js +141 -0
- package/dist-client/modeller/figure-v3-capability.js.map +1 -0
- package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
- package/dist-client/modeller/figure-v3-clips.js +268 -0
- package/dist-client/modeller/figure-v3-clips.js.map +1 -0
- package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
- package/dist-client/modeller/figure-v3-controls.js +226 -0
- package/dist-client/modeller/figure-v3-controls.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +10 -2
- package/dist-client/modeller/figure-v3-links.js +134 -79
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +5 -2
- package/dist-client/modeller/figure-v3-material.js +109 -35
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
- package/dist-client/modeller/figure-v3-parts.js +88 -3
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
- package/dist-client/modeller/figure-v3-preview.js +322 -0
- package/dist-client/modeller/figure-v3-preview.js.map +1 -0
- package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
- package/dist-client/modeller/figure-v3-properties.js +29 -57
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
- package/dist-client/modeller/figure-v3-verdict.js +579 -59
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +118 -0
- package/dist-client/modeller/figure-view.js +78 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.d.ts +23 -0
- package/dist-client/modeller/side-panel-styles.js +326 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +29 -37
- package/dist-client/modeller/v3-canvas.js +188 -260
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +39 -0
- package/dist-client/modeller/v3-clips.js +58 -0
- package/dist-client/modeller/v3-clips.js.map +1 -0
- package/dist-client/modeller/v3-controls.d.ts +28 -0
- package/dist-client/modeller/v3-controls.js +28 -0
- package/dist-client/modeller/v3-controls.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +58 -1
- package/dist-client/modeller/v3-editor.js +556 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +30 -0
- package/dist-client/modeller/v3-fields.js +46 -0
- package/dist-client/modeller/v3-fields.js.map +1 -0
- package/dist-client/modeller/v3-help.d.ts +18 -0
- package/dist-client/modeller/v3-help.js +37 -0
- package/dist-client/modeller/v3-help.js.map +1 -0
- package/dist-client/modeller/v3-keys.d.ts +14 -0
- package/dist-client/modeller/v3-keys.js +23 -0
- package/dist-client/modeller/v3-keys.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +44 -2
- package/dist-client/modeller/v3-links.js +125 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +31 -0
- package/dist-client/modeller/v3-live.js +58 -0
- package/dist-client/modeller/v3-live.js.map +1 -0
- package/dist-client/modeller/v3-maturity.d.ts +120 -0
- package/dist-client/modeller/v3-maturity.js +317 -0
- package/dist-client/modeller/v3-maturity.js.map +1 -0
- package/dist-client/modeller/v3-palette.d.ts +7 -0
- package/dist-client/modeller/v3-palette.js +42 -0
- package/dist-client/modeller/v3-palette.js.map +1 -0
- package/dist-client/modeller/v3-preview-model.d.ts +32 -0
- package/dist-client/modeller/v3-preview-model.js +48 -0
- package/dist-client/modeller/v3-preview-model.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +31 -0
- package/dist-client/modeller/v3-stage-model.js +121 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -0
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
- package/dist-client/pages/figure-v3-modeller-page.js +64 -136
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
- package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/v3-clips.test.ts +70 -0
- package/test/v3-controls.test.ts +68 -0
- package/test/v3-fields.test.ts +62 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +118 -7
- package/test/v3-live.test.ts +99 -0
- package/test/v3-material.test.ts +6 -0
- package/test/v3-maturity.test.ts +268 -0
- package/test/v3-parts-wiring.test.ts +3 -1
- package/test/v3-preview.test.ts +42 -0
- package/test/v3-properties.test.ts +4 -2
- package/test/v3-scene-canvas.test.ts +85 -0
- package/test/v3-stage-model.test.ts +25 -0
- package/test/v3-theme.test.ts +16 -25
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +98 -5
- package/translations/ja.json +100 -7
- package/translations/ko.json +98 -5
- package/translations/ms.json +100 -7
- package/translations/zh.json +100 -7
|
@@ -7,7 +7,29 @@ import { i18next, localize } from '@operato/i18n'
|
|
|
7
7
|
import { V3_FACES } from '@hatiolab/figure-model'
|
|
8
8
|
import type { V3Asset, V3AuthoringAction, V3DimensionSource, V3Face } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
acceptsOf,
|
|
12
|
+
AXES,
|
|
13
|
+
canFasten,
|
|
14
|
+
canFollow,
|
|
15
|
+
dimensionNow,
|
|
16
|
+
followableAxes,
|
|
17
|
+
positionIsShare,
|
|
18
|
+
choiceOf,
|
|
19
|
+
fastenedTo,
|
|
20
|
+
followOf,
|
|
21
|
+
hasInstanceSize,
|
|
22
|
+
holdsThings,
|
|
23
|
+
motionOf,
|
|
24
|
+
placeFacesOf,
|
|
25
|
+
placeIdFor,
|
|
26
|
+
placesOn,
|
|
27
|
+
positionsWorkedOut,
|
|
28
|
+
type Axis,
|
|
29
|
+
type Follow
|
|
30
|
+
} from './v3-links.js'
|
|
31
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
32
|
+
import { HelpToggles } from './v3-help.js'
|
|
11
33
|
|
|
12
34
|
const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
13
35
|
|
|
@@ -26,76 +48,25 @@ const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
|
26
48
|
*/
|
|
27
49
|
@customElement('figure-v3-links')
|
|
28
50
|
export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
29
|
-
|
|
51
|
+
private readonly help = new HelpToggles(this)
|
|
52
|
+
static styles = [SidePanelStyles, css`
|
|
30
53
|
:host {
|
|
31
|
-
display:
|
|
32
|
-
gap: 10px;
|
|
54
|
+
display: block;
|
|
33
55
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
/* The measure's name, then its rule: X, or a cylinder's 반지름 · 길이, on one line. */
|
|
57
|
+
div[row] > b {
|
|
58
|
+
white-space: nowrap;
|
|
37
59
|
}
|
|
38
60
|
div[row] {
|
|
39
61
|
display: grid;
|
|
40
|
-
grid-template-columns: 24px minmax(0, 1fr);
|
|
41
|
-
gap: 6px;
|
|
42
|
-
align-items: center;
|
|
43
|
-
}
|
|
44
|
-
div[form] {
|
|
45
|
-
display: grid;
|
|
46
|
-
grid-template-columns: 96px minmax(0, 1fr);
|
|
62
|
+
grid-template-columns: minmax(24px, max-content) minmax(0, 1fr);
|
|
47
63
|
gap: 6px;
|
|
48
64
|
align-items: center;
|
|
49
65
|
}
|
|
50
|
-
div[pair] {
|
|
51
|
-
display: flex;
|
|
52
|
-
gap: 6px;
|
|
53
|
-
}
|
|
54
|
-
div[pair] > * {
|
|
55
|
-
flex: 1;
|
|
56
|
-
min-width: 0;
|
|
57
|
-
}
|
|
58
|
-
select,
|
|
59
|
-
input,
|
|
60
|
-
button {
|
|
61
|
-
font: inherit;
|
|
62
|
-
min-width: 0;
|
|
63
|
-
}
|
|
64
|
-
button[act] {
|
|
65
|
-
justify-self: start;
|
|
66
|
-
display: inline-flex;
|
|
67
|
-
align-items: center;
|
|
68
|
-
gap: 4px;
|
|
69
|
-
padding: 4px 10px;
|
|
70
|
-
border: none;
|
|
71
|
-
border-radius: 999em;
|
|
72
|
-
cursor: pointer;
|
|
73
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
74
|
-
background: var(--md-sys-color-secondary-container);
|
|
75
|
-
}
|
|
76
|
-
button[act]:disabled {
|
|
77
|
-
opacity: 0.5;
|
|
78
|
-
cursor: default;
|
|
79
|
-
}
|
|
80
|
-
button[act] md-icon {
|
|
81
|
-
font-size: 16px;
|
|
82
|
-
width: 16px;
|
|
83
|
-
height: 16px;
|
|
84
|
-
}
|
|
85
|
-
small,
|
|
86
|
-
p[hint] {
|
|
87
|
-
margin: 0;
|
|
88
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
89
|
-
}
|
|
90
66
|
b[bound] {
|
|
91
67
|
font-weight: 600;
|
|
92
68
|
}
|
|
93
|
-
|
|
94
|
-
display: flex;
|
|
95
|
-
align-items: center;
|
|
96
|
-
gap: 6px;
|
|
97
|
-
}
|
|
98
|
-
`
|
|
69
|
+
`]
|
|
99
70
|
|
|
100
71
|
/** The asset being edited. */
|
|
101
72
|
@property({ attribute: false }) asset?: V3Asset
|
|
@@ -104,37 +75,124 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
104
75
|
/** The part's own extent along its axes now, in mm — what a dimension is fixed at when it stops following. */
|
|
105
76
|
@property({ attribute: false }) dims?: [number, number, number]
|
|
106
77
|
@property({ type: Boolean }) locked = false
|
|
78
|
+
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
79
|
+
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
107
80
|
|
|
108
81
|
@state() private fasten = { face: 'bottom' as V3Face, toPlane: false, target: '', targetFace: 'top' as V3Face, facing: 'meet' as 'meet' | 'flush', gap: 0 }
|
|
109
82
|
@state() private motion = { kind: 'slide' as 'slide' | 'turn', axis: 'x' as Axis, id: '', min: 0, max: 500 }
|
|
83
|
+
@state() private place = { role: 'grip' as 'seat' | 'grip', face: 'top' as V3Face, label: '', accepts: '', capacity: 1 }
|
|
110
84
|
|
|
111
85
|
render() {
|
|
112
86
|
const asset = this.asset
|
|
113
87
|
if (!asset || !this.partId) return nothing
|
|
88
|
+
if (this.section === 'motion')
|
|
89
|
+
return html`<section><h3>${i18next.t('figure.label.motion')}${this.help.button('figure.text.motion-explained')}</h3>${this.help.hint('figure.text.motion-explained')}
|
|
90
|
+
${this.renderMotion(asset)}</section>`
|
|
91
|
+
if (this.section === 'places')
|
|
92
|
+
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')}
|
|
93
|
+
${placeFacesOf(asset, this.partId).length ? this.renderPlaces(asset) : html`<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`
|
|
94
|
+
return html`
|
|
95
|
+
<section>
|
|
96
|
+
<h3>${i18next.t('figure.label.dimensions')}</h3>
|
|
97
|
+
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
98
|
+
</section>
|
|
99
|
+
<section>
|
|
100
|
+
<h3>${i18next.t('figure.label.placement')}${this.help.button('figure.text.fastening-explained')}</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
101
|
+
${this.renderPositions(asset)}
|
|
102
|
+
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
103
|
+
</section>
|
|
104
|
+
`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Where things are held on this part: a seat things rest on, or a grip things are held in (parity row ⑤). */
|
|
108
|
+
private renderPlaces(asset: V3Asset) {
|
|
109
|
+
const faces = placeFacesOf(asset, this.partId)
|
|
110
|
+
const p = this.place
|
|
111
|
+
const face = faces.includes(p.face) ? p.face : faces[0]!
|
|
112
|
+
const accepts = acceptsOf(p.accepts)
|
|
113
|
+
const capacityOk = Number.isSafeInteger(p.capacity) && p.capacity >= 1
|
|
114
|
+
const roleName = (role: string) => i18next.t('figure.label.place-' + role)
|
|
114
115
|
return html`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
${placesOn(asset, this.partId).map(
|
|
117
|
+
one => html`<div pair>
|
|
118
|
+
<span>
|
|
119
|
+
<b bound>${one.label}</b>
|
|
120
|
+
<small>${roleName(one.role)} · ${one.face ? i18next.t('figure.label.face-' + one.face) : ''} · ${one.accepts.join(', ')} · ${i18next.t('figure.label.place-holds', { count: one.capacity })}</small>
|
|
121
|
+
</span>
|
|
122
|
+
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-binding', id: one.id })}>
|
|
123
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-place')}
|
|
124
|
+
</button>
|
|
125
|
+
</div>`
|
|
126
|
+
)}
|
|
127
|
+
${holdsThings(asset)
|
|
128
|
+
? nothing
|
|
129
|
+
: html`<p hint>${i18next.t('figure.text.place-needs-holding')}</p>
|
|
130
|
+
<button act ?disabled=${this.locked} @click=${() => this.declareHolding(asset)}>
|
|
131
|
+
<md-icon>inventory_2</md-icon>${i18next.t('figure.button.declare-holding')}
|
|
132
|
+
</button>`}
|
|
133
|
+
<div pair>
|
|
134
|
+
<select @change=${(e: Event) => (this.place = { ...p, role: (e.target as HTMLSelectElement).value as 'seat' | 'grip' })}>
|
|
135
|
+
<option value="grip" ?selected=${p.role === 'grip'}>${roleName('grip')}</option>
|
|
136
|
+
<option value="seat" ?selected=${p.role === 'seat'}>${roleName('seat')}</option>
|
|
137
|
+
</select>
|
|
138
|
+
<select @change=${(e: Event) => (this.place = { ...p, face: (e.target as HTMLSelectElement).value as V3Face })}>
|
|
139
|
+
${faces.map(one => html`<option value=${one} ?selected=${one === face}>${i18next.t('figure.label.face-' + one)}</option>`)}
|
|
140
|
+
</select>
|
|
141
|
+
</div>
|
|
142
|
+
<div form>
|
|
143
|
+
<span>${i18next.t('figure.label.place-name')}</span>
|
|
144
|
+
<input .value=${p.label} placeholder=${roleName(p.role)} @change=${(e: Event) => (this.place = { ...p, label: (e.target as HTMLInputElement).value })} />
|
|
145
|
+
<span>${i18next.t('figure.label.place-accepts')}</span>
|
|
146
|
+
<input .value=${p.accepts} placeholder="BOX, TOTE" @input=${(e: Event) => (this.place = { ...p, accepts: (e.target as HTMLInputElement).value })} />
|
|
147
|
+
<span>${i18next.t('figure.label.place-capacity')}</span>
|
|
148
|
+
<input type="number" min="1" step="1" .value=${String(p.capacity)} @change=${(e: Event) => (this.place = { ...p, capacity: (e.target as HTMLInputElement).valueAsNumber })} />
|
|
149
|
+
</div>
|
|
150
|
+
${accepts.length ? nothing : html`<small>${i18next.t('figure.text.place-accepts-needed')}</small>`}
|
|
151
|
+
<button
|
|
152
|
+
act
|
|
153
|
+
?disabled=${this.locked || !accepts.length || !capacityOk}
|
|
154
|
+
@click=${() =>
|
|
155
|
+
this.command({
|
|
156
|
+
kind: 'add-binding',
|
|
157
|
+
id: placeIdFor(asset, this.partId, p.role),
|
|
158
|
+
label: p.label.trim() || roleName(p.role),
|
|
159
|
+
role: p.role,
|
|
160
|
+
part: this.partId,
|
|
161
|
+
face,
|
|
162
|
+
accepts,
|
|
163
|
+
capacity: p.capacity
|
|
164
|
+
})}
|
|
165
|
+
>
|
|
166
|
+
<md-icon>back_hand</md-icon>${i18next.t('figure.button.add-place')}
|
|
167
|
+
</button>
|
|
122
168
|
`
|
|
123
169
|
}
|
|
124
170
|
|
|
171
|
+
/** The figure holds things: `Capacity` and `Holdable`, the ladder's middle rung. One part edit, one undo step. */
|
|
172
|
+
private declareHolding(asset: V3Asset) {
|
|
173
|
+
const names = [...new Set([...(asset.document.capabilities.names ?? []), 'Capacity', 'Holdable'])]
|
|
174
|
+
this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit: { kind: 'set-capabilities', names } }, bubbles: true, composed: true }))
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
private renderDimensions(asset: V3Asset) {
|
|
126
178
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
|
|
127
|
-
|
|
179
|
+
const round = followableAxes(asset, this.partId).length === 2
|
|
180
|
+
return followableAxes(asset, this.partId).map(axis => {
|
|
128
181
|
const now = followOf(asset, this.partId, axis)
|
|
129
182
|
const value = choiceOf(now)
|
|
183
|
+
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
184
|
+
const name = round ? i18next.t(axis === 'x' ? 'figure.label.field-radius' : 'figure.label.field-length') : axis.toUpperCase()
|
|
130
185
|
return html`<div row>
|
|
131
|
-
<b>${
|
|
132
|
-
<select ?disabled=${this.locked
|
|
186
|
+
<b>${name}</b>
|
|
187
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => this.follow(asset, axis, (e.target as HTMLSelectElement).value, now)}>
|
|
133
188
|
<option value="own" ?selected=${value === 'own'}>${i18next.t('figure.label.follows-nothing')}</option>
|
|
134
189
|
${AXES.filter(one => hasInstanceSize(asset, one)).map(
|
|
135
|
-
one => html`<option value=${`
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
one => html`<option value=${`share:${one}`} ?selected=${value === `share:${one}`}>
|
|
191
|
+
${i18next.t('figure.label.follows-instance-share')} · ${one.toUpperCase()}
|
|
192
|
+
</option>
|
|
193
|
+
<option value=${`instance:${one}`} ?selected=${value === `instance:${one}`}>
|
|
194
|
+
${i18next.t('figure.label.follows-instance')} · ${one.toUpperCase()}
|
|
195
|
+
</option>`
|
|
138
196
|
)}
|
|
139
197
|
${others.flatMap(other =>
|
|
140
198
|
AXES.map(
|
|
@@ -149,15 +207,24 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
149
207
|
})
|
|
150
208
|
}
|
|
151
209
|
|
|
152
|
-
private follow(axis: Axis, choice: string, now: Follow) {
|
|
210
|
+
private follow(asset: V3Asset, axis: Axis, choice: string, now: Follow) {
|
|
153
211
|
const following = now.of !== 'own'
|
|
212
|
+
/* The measure as it stands: a dimension stops following at it, and a share is taken of it. */
|
|
213
|
+
const current = dimensionNow(asset, this.partId, axis) ?? this.dims?.[AXES.indexOf(axis)] ?? 100
|
|
154
214
|
if (choice === 'own') {
|
|
155
215
|
if (!following) return
|
|
156
|
-
|
|
157
|
-
this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm })
|
|
216
|
+
this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })
|
|
158
217
|
return
|
|
159
218
|
}
|
|
160
219
|
const [of, a, b] = choice.split(':')
|
|
220
|
+
if (of === 'share') {
|
|
221
|
+
/* V2's proportional rule: this measure is the share of the instance size it is now, so it grows with the figure. */
|
|
222
|
+
const size = asset.designInputs[`size.${a}`]
|
|
223
|
+
if (!size) return
|
|
224
|
+
const times = Math.round((current / size) * 1e6) / 1e6
|
|
225
|
+
this.command({ kind: 'link-dimension', part: this.partId, axis, source: { of: 'instance', axis: a as Axis }, times, replace: following })
|
|
226
|
+
return
|
|
227
|
+
}
|
|
161
228
|
const source: V3DimensionSource = of === 'instance' ? { of: 'instance', axis: a as Axis } : { of: 'part', part: a!, axis: b as Axis }
|
|
162
229
|
this.command({ kind: 'link-dimension', part: this.partId, axis, source, replace: following })
|
|
163
230
|
}
|
|
@@ -169,10 +236,15 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
169
236
|
private renderPositions(asset: V3Asset) {
|
|
170
237
|
return positionsWorkedOut(asset, this.partId).map(
|
|
171
238
|
axis => html`<div pair>
|
|
172
|
-
<span>${axis.toUpperCase()} · ${i18next.t('figure.label.position-worked-out')}</span>
|
|
239
|
+
<span>${axis.toUpperCase()} · ${i18next.t(positionIsShare(asset, this.partId, axis) ? 'figure.label.position-in-proportion' : 'figure.label.position-worked-out')}</span>
|
|
173
240
|
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'fix-position', part: this.partId, axis })}>
|
|
174
241
|
<md-icon>push_pin</md-icon>${i18next.t('figure.button.fix-position')}
|
|
175
242
|
</button>
|
|
243
|
+
${hasInstanceSize(asset, axis) && !positionIsShare(asset, this.partId, axis)
|
|
244
|
+
? html`<button act ?disabled=${this.locked} title=${i18next.t('figure.text.scale-position-explained')} @click=${() => this.command({ kind: 'scale-position', part: this.partId, axis })}>
|
|
245
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.scale-position')}
|
|
246
|
+
</button>`
|
|
247
|
+
: nothing}
|
|
176
248
|
</div>`
|
|
177
249
|
)
|
|
178
250
|
}
|
|
@@ -189,7 +261,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
189
261
|
<md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
|
|
190
262
|
</button>
|
|
191
263
|
</div>`
|
|
192
|
-
:
|
|
264
|
+
: nothing}
|
|
193
265
|
<div form>
|
|
194
266
|
<span>${i18next.t('figure.label.my-face')}</span>
|
|
195
267
|
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as V3Face })}>
|
|
@@ -257,7 +329,6 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
257
329
|
const m = this.motion
|
|
258
330
|
const unit = m.kind === 'turn' ? 'deg' : 'mm'
|
|
259
331
|
return html`
|
|
260
|
-
<p hint>${i18next.t('figure.text.motion-explained')}</p>
|
|
261
332
|
<div pair>
|
|
262
333
|
<select @change=${(e: Event) => this.setKind((e.target as HTMLSelectElement).value as 'slide' | 'turn')}>
|
|
263
334
|
<option value="slide" ?selected=${m.kind === 'slide'}>${i18next.t('figure.label.motion-slide')}</option>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { css, html, LitElement, nothing } from 'lit'
|
|
2
|
-
import { customElement, property } from 'lit/decorators.js'
|
|
1
|
+
import { css, html, LitElement, nothing, svg } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
3
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { MATERIAL_PRESETS, SEGMENT_PRESETS, v3TrianglesOf } from '@hatiolab/figure-model'
|
|
6
6
|
import { activePalette } from '@hatiolab/things-scene'
|
|
7
7
|
import type { V3AssetGeometry, V3Material, V3PartEdit } from '@hatiolab/figure-model'
|
|
8
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
8
9
|
|
|
9
10
|
/** The purpose each palette role is for, by its token — the V2 inspector's words for the same roles. */
|
|
10
11
|
const PURPOSE: Record<string, string> = {
|
|
@@ -40,14 +41,9 @@ const DEFAULT_SEGMENTS = 12
|
|
|
40
41
|
*/
|
|
41
42
|
@customElement('figure-v3-material')
|
|
42
43
|
export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
43
|
-
static styles = css`
|
|
44
|
+
static styles = [SidePanelStyles, css`
|
|
44
45
|
:host {
|
|
45
|
-
display:
|
|
46
|
-
gap: 10px;
|
|
47
|
-
}
|
|
48
|
-
h3 {
|
|
49
|
-
font-size: var(--ui-t-body, 13px);
|
|
50
|
-
margin: 12px 0 0;
|
|
46
|
+
display: block;
|
|
51
47
|
}
|
|
52
48
|
div[field] {
|
|
53
49
|
display: grid;
|
|
@@ -74,46 +70,96 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
74
70
|
outline: 2px solid var(--md-sys-color-primary);
|
|
75
71
|
outline-offset: 1px;
|
|
76
72
|
}
|
|
77
|
-
small {
|
|
78
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
79
|
-
}
|
|
80
73
|
select,
|
|
81
74
|
input[type='range'] {
|
|
82
75
|
font: inherit;
|
|
83
76
|
min-width: 0;
|
|
84
77
|
}
|
|
85
|
-
|
|
86
|
-
display: flex;
|
|
87
|
-
align-items: center;
|
|
88
|
-
gap: 6px;
|
|
89
|
-
}
|
|
78
|
+
/* V2's segment picker: the four common counts as their silhouettes with their triangle cost, the rest folded. */
|
|
90
79
|
div[segments] {
|
|
91
80
|
display: grid;
|
|
92
|
-
grid-template-columns: repeat(
|
|
81
|
+
grid-template-columns: repeat(4, 1fr);
|
|
93
82
|
gap: 4px;
|
|
94
83
|
}
|
|
95
|
-
|
|
96
|
-
display:
|
|
97
|
-
|
|
84
|
+
button[seg] {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 1px;
|
|
89
|
+
padding: 6px 2px 4px;
|
|
98
90
|
font: inherit;
|
|
91
|
+
color: var(--md-sys-color-on-surface);
|
|
92
|
+
background: none;
|
|
99
93
|
border: 1px solid var(--md-sys-color-outline-variant);
|
|
100
94
|
border-radius: 6px;
|
|
101
|
-
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
}
|
|
97
|
+
button[seg]:hover:not([on]) {
|
|
98
|
+
background-color: var(--md-sys-color-surface-container);
|
|
99
|
+
}
|
|
100
|
+
button[seg][on] {
|
|
101
|
+
color: var(--md-sys-color-on-primary-container);
|
|
102
|
+
background-color: var(--md-sys-color-primary-container);
|
|
103
|
+
border-color: transparent;
|
|
104
|
+
}
|
|
105
|
+
/* Small, and 12 and 16 both read as circles: large enough for the corners to show (V2). */
|
|
106
|
+
button[seg] svg {
|
|
107
|
+
display: block;
|
|
108
|
+
width: 32px;
|
|
109
|
+
height: 32px;
|
|
110
|
+
}
|
|
111
|
+
button[seg] svg polygon {
|
|
112
|
+
fill: var(--md-sys-color-surface-container-highest);
|
|
113
|
+
stroke: currentColor;
|
|
114
|
+
stroke-width: 1.1;
|
|
115
|
+
stroke-linejoin: miter;
|
|
116
|
+
}
|
|
117
|
+
button[seg][on] svg polygon {
|
|
118
|
+
fill: var(--md-sys-color-surface-container-lowest);
|
|
119
|
+
}
|
|
120
|
+
button[seg] b {
|
|
121
|
+
font-size: 0.8rem;
|
|
122
|
+
}
|
|
123
|
+
button[seg] small {
|
|
124
|
+
margin: 0;
|
|
125
|
+
font-size: 0.6rem;
|
|
126
|
+
}
|
|
127
|
+
details[free] summary {
|
|
128
|
+
margin-top: 4px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
font-size: 0.68rem;
|
|
131
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
132
|
+
}
|
|
133
|
+
div[presets] {
|
|
134
|
+
display: flex;
|
|
135
|
+
gap: 4px;
|
|
136
|
+
margin-top: 4px;
|
|
137
|
+
}
|
|
138
|
+
div[presets] button {
|
|
139
|
+
flex: 1;
|
|
140
|
+
padding: 3px 0;
|
|
141
|
+
font: inherit;
|
|
142
|
+
font-size: 0.72rem;
|
|
102
143
|
color: var(--md-sys-color-on-surface);
|
|
144
|
+
background: none;
|
|
145
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
146
|
+
border-radius: 6px;
|
|
103
147
|
cursor: pointer;
|
|
104
148
|
}
|
|
105
|
-
div[
|
|
106
|
-
|
|
107
|
-
color: var(--md-sys-color-
|
|
108
|
-
border-color:
|
|
149
|
+
div[presets] button[on] {
|
|
150
|
+
color: var(--md-sys-color-on-primary-container);
|
|
151
|
+
background-color: var(--md-sys-color-primary-container);
|
|
152
|
+
border-color: transparent;
|
|
109
153
|
}
|
|
110
|
-
`
|
|
154
|
+
`]
|
|
111
155
|
|
|
112
156
|
/** The selected body as evaluated — its material, its segments and its shape. */
|
|
113
157
|
@property({ attribute: false }) body?: V3AssetGeometry & { id: string }
|
|
114
158
|
/** The asset's palette, token → colour. */
|
|
115
159
|
@property({ attribute: false }) palette: Record<string, string> = {}
|
|
116
160
|
@property({ type: Boolean }) locked = false
|
|
161
|
+
/** The glow while its slider is dragged: shown beside it and drawn on the canvas, recorded on release. */
|
|
162
|
+
@state() private glowing?: number
|
|
117
163
|
|
|
118
164
|
render() {
|
|
119
165
|
const part = this.body
|
|
@@ -127,7 +173,7 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
127
173
|
const painted = entries.filter(([token]) => !token.startsWith('palette.status-'))
|
|
128
174
|
const lit = entries.filter(([token]) => token.startsWith('palette.status-'))
|
|
129
175
|
const glow = material.emissive
|
|
130
|
-
return html
|
|
176
|
+
return html`<section>
|
|
131
177
|
<h3>${i18next.t('figure.label.part-material')}</h3>
|
|
132
178
|
<div field>
|
|
133
179
|
<label>${i18next.t('figure.label.palette-role')}</label>
|
|
@@ -168,9 +214,10 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
168
214
|
step="0.05"
|
|
169
215
|
?disabled=${this.locked}
|
|
170
216
|
.value=${String(glow?.intensity ?? 0)}
|
|
217
|
+
@input=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value), true)}
|
|
171
218
|
@change=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value))}
|
|
172
219
|
/>
|
|
173
|
-
<small>${(glow?.intensity ?? 0).toFixed(2)}</small>
|
|
220
|
+
<small>${(this.glowing ?? glow?.intensity ?? 0).toFixed(2)}</small>
|
|
174
221
|
</div>
|
|
175
222
|
${glow
|
|
176
223
|
? html`<div field>
|
|
@@ -179,7 +226,7 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
179
226
|
</div>`
|
|
180
227
|
: nothing}
|
|
181
228
|
${this.renderSegments(part)}
|
|
182
|
-
|
|
229
|
+
</section>`
|
|
183
230
|
}
|
|
184
231
|
|
|
185
232
|
private renderSegments(part: V3AssetGeometry & { id: string }) {
|
|
@@ -190,17 +237,29 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
190
237
|
<div field>
|
|
191
238
|
<label>${i18next.t('figure.label.segments')}</label>
|
|
192
239
|
<div segments>
|
|
193
|
-
${SEGMENT_PRESETS.map(
|
|
240
|
+
${SEGMENT_PRESETS.slice(0, 4).map(
|
|
194
241
|
preset => html`<button
|
|
242
|
+
seg
|
|
195
243
|
?on=${preset === current}
|
|
196
244
|
?disabled=${this.locked}
|
|
197
245
|
@click=${() => this.edit({ kind: 'set-segments', target: part.id, segments: preset })}
|
|
198
246
|
>
|
|
247
|
+
${silhouette(preset)}
|
|
199
248
|
<b>${preset}</b>
|
|
200
249
|
<small>${i18next.t('figure.text.n-triangles', { count: v3TrianglesOf({ ...part, segments: preset }) })}</small>
|
|
201
250
|
</button>`
|
|
202
251
|
)}
|
|
203
252
|
</div>
|
|
253
|
+
<details free ?open=${SEGMENT_PRESETS.slice(4).includes(current as never)}>
|
|
254
|
+
<summary>${i18next.t('figure.text.more-segment-options')}</summary>
|
|
255
|
+
<div presets>
|
|
256
|
+
${SEGMENT_PRESETS.slice(4).map(
|
|
257
|
+
preset => html`<button ?on=${preset === current} ?disabled=${this.locked} @click=${() => this.edit({ kind: 'set-segments', target: part.id, segments: preset })}>
|
|
258
|
+
${preset}
|
|
259
|
+
</button>`
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</details>
|
|
204
263
|
</div>
|
|
205
264
|
`
|
|
206
265
|
}
|
|
@@ -224,12 +283,14 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
224
283
|
}
|
|
225
284
|
|
|
226
285
|
/** Intensity 0 is no glow at all, as in V2; a new glow takes the part's own colour until another is picked. */
|
|
227
|
-
private glow(intensity: number) {
|
|
286
|
+
private glow(intensity: number, live = false) {
|
|
228
287
|
const material = this.body!.material
|
|
229
|
-
this.
|
|
288
|
+
this.glowing = live ? intensity : undefined
|
|
289
|
+
this.put({ emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined }, live)
|
|
230
290
|
}
|
|
231
291
|
|
|
232
|
-
|
|
292
|
+
/** A material change: recorded, or -- while a slider is dragged -- only drawn (`v3-part-live`). */
|
|
293
|
+
private put(patch: Partial<V3Material>, live = false) {
|
|
233
294
|
const material: V3Material = { ...this.body!.material, ...patch }
|
|
234
295
|
for (const key of Object.keys(material) as (keyof V3Material)[]) if (material[key] === undefined) delete material[key]
|
|
235
296
|
/* The colours this material names that the asset does not carry yet. */
|
|
@@ -239,10 +300,22 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
239
300
|
.filter((token): token is string => !!token && this.palette[token] === undefined && offered[token] !== undefined)
|
|
240
301
|
.map(token => [token, offered[token]!])
|
|
241
302
|
)
|
|
242
|
-
|
|
303
|
+
const edit: V3PartEdit = { kind: 'set-material', target: this.body!.id, material, ...(Object.keys(palette).length ? { palette } : {}) }
|
|
304
|
+
if (live) this.dispatchEvent(new CustomEvent('v3-part-live', { detail: { edit }, bubbles: true, composed: true }))
|
|
305
|
+
else this.edit(edit)
|
|
243
306
|
}
|
|
244
307
|
|
|
245
308
|
private edit(edit: V3PartEdit) {
|
|
246
309
|
this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit }, bubbles: true, composed: true }))
|
|
247
310
|
}
|
|
248
311
|
}
|
|
312
|
+
|
|
313
|
+
/** A regular polygon of this many sides -- how round a body with this rim count looks from its end (V2). */
|
|
314
|
+
function silhouette(segments: number) {
|
|
315
|
+
const points: string[] = []
|
|
316
|
+
for (let i = 0; i < segments; i++) {
|
|
317
|
+
const angle = -Math.PI / 2 + (i * 2 * Math.PI) / segments
|
|
318
|
+
points.push(`${(16 + 13 * Math.cos(angle)).toFixed(2)},${(16 + 13 * Math.sin(angle)).toFixed(2)}`)
|
|
319
|
+
}
|
|
320
|
+
return svg`<svg viewBox="0 0 32 32" aria-hidden="true"><polygon points=${points.join(' ')}></polygon></svg>`
|
|
321
|
+
}
|