@things-factory/figure-ui 10.1.155 → 10.1.157
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/clips.ts +43 -4
- package/client/modeller/data-interface.ts +110 -0
- package/client/modeller/editor.ts +16 -2
- package/client/modeller/figure-clips.ts +125 -12
- package/client/modeller/figure-data-panel.ts +308 -0
- package/client/modeller/figure-preview.ts +41 -4
- package/client/modeller/refusals.ts +46 -0
- package/dist-client/modeller/clips.d.ts +32 -2
- package/dist-client/modeller/clips.js +32 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +42 -0
- package/dist-client/modeller/data-interface.js +95 -0
- package/dist-client/modeller/data-interface.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +3 -0
- package/dist-client/modeller/editor.js +18 -0
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +10 -0
- package/dist-client/modeller/figure-clips.js +125 -10
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +39 -0
- package/dist-client/modeller/figure-data-panel.js +317 -0
- package/dist-client/modeller/figure-data-panel.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +7 -0
- package/dist-client/modeller/figure-preview.js +49 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +5 -0
- package/dist-client/modeller/refusals.js +58 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/data-motion.test.ts +135 -0
- package/translations/en.json +85 -2
- package/translations/es.json +84 -1
- package/translations/ja.json +85 -2
- package/translations/ko.json +85 -2
- package/translations/ms.json +85 -2
- package/translations/zh.json +85 -2
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
|
+
|
|
6
|
+
import { i18next, localize } from '@operato/i18n'
|
|
7
|
+
import { FIGURE_UNITS } from '@hatiolab/figure-model'
|
|
8
|
+
import type { FigureAsset, FigureDataConnection, FigureDataProperty, FigureDataValue } from '@hatiolab/figure-model'
|
|
9
|
+
|
|
10
|
+
import { connectionDraftOf, dataSampleOf, dataTargetsOf, domainOf, propertyDraftOf, sampleValueOf, targetKeyOf, type DataTargetChoice } from './data-interface.js'
|
|
11
|
+
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
12
|
+
import { HelpToggles } from './help.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 데이터 -- the figure's public data properties and what each moves inside it (ADR-0051 §2.2). A property is a value a
|
|
16
|
+
* consumer maps its own data onto, by its id: a number with a unit and a range, a boolean, or an enum. A connection
|
|
17
|
+
* makes a property move a control, a colour, or which clip of a motion group plays and how fast. Every change is one
|
|
18
|
+
* part edit, and whether it holds is the kernel's check.
|
|
19
|
+
*
|
|
20
|
+
* The sample is not saved: values tried here, what they would move (figure-model's evaluation), and -- in the preview --
|
|
21
|
+
* the figure running on them (`data-sample`).
|
|
22
|
+
*/
|
|
23
|
+
@customElement('figure-data-panel')
|
|
24
|
+
export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
25
|
+
private readonly help = new HelpToggles(this)
|
|
26
|
+
static styles = [SidePanelStyles, css`
|
|
27
|
+
:host {
|
|
28
|
+
display: block;
|
|
29
|
+
}
|
|
30
|
+
section[item] {
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: 8px;
|
|
33
|
+
padding: 8px;
|
|
34
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
35
|
+
border-radius: 8px;
|
|
36
|
+
}
|
|
37
|
+
div[head] {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: minmax(0, 1fr) 30px;
|
|
40
|
+
gap: 6px;
|
|
41
|
+
align-items: center;
|
|
42
|
+
}
|
|
43
|
+
div[head] strong {
|
|
44
|
+
min-width: 0;
|
|
45
|
+
overflow-wrap: anywhere;
|
|
46
|
+
}
|
|
47
|
+
div[head] button[act] {
|
|
48
|
+
width: 30px;
|
|
49
|
+
min-height: var(--figure-control-height);
|
|
50
|
+
margin: 0;
|
|
51
|
+
padding: 0;
|
|
52
|
+
}
|
|
53
|
+
div[cases] {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
|
|
56
|
+
gap: 4px 6px;
|
|
57
|
+
align-items: center;
|
|
58
|
+
}
|
|
59
|
+
ul[result] {
|
|
60
|
+
margin: 0;
|
|
61
|
+
padding-left: 16px;
|
|
62
|
+
}
|
|
63
|
+
md-icon {
|
|
64
|
+
font-size: 16px;
|
|
65
|
+
width: 16px;
|
|
66
|
+
height: 16px;
|
|
67
|
+
}
|
|
68
|
+
`]
|
|
69
|
+
|
|
70
|
+
@property({ attribute: false }) asset?: FigureAsset
|
|
71
|
+
@property({ type: Boolean }) locked = false
|
|
72
|
+
|
|
73
|
+
@state() private adding = { label: '', kind: 'number' as FigureDataProperty['kind'] }
|
|
74
|
+
@state() private connecting = { property: '', target: '' }
|
|
75
|
+
/** The sample's values as typed, by property id; a property left out reads its default. */
|
|
76
|
+
@state() private sample: Record<string, FigureDataValue> = {}
|
|
77
|
+
@state() private sampling = false
|
|
78
|
+
|
|
79
|
+
render() {
|
|
80
|
+
const asset = this.asset
|
|
81
|
+
if (!asset) return nothing
|
|
82
|
+
const properties = asset.dataInterface?.properties ?? []
|
|
83
|
+
const connections = asset.dataInterface?.connections ?? []
|
|
84
|
+
return html`
|
|
85
|
+
<section>
|
|
86
|
+
<h3>${i18next.t('figure.label.data-properties')}${this.help.button('figure.text.data-properties-explained')}</h3>
|
|
87
|
+
${this.help.hint('figure.text.data-properties-explained')}
|
|
88
|
+
${properties.map(p => this.renderProperty(p))}
|
|
89
|
+
${this.renderAddProperty(asset)}
|
|
90
|
+
</section>
|
|
91
|
+
<section>
|
|
92
|
+
<h3>${i18next.t('figure.label.data-connections')}${this.help.button('figure.text.data-connections-explained')}</h3>
|
|
93
|
+
${this.help.hint('figure.text.data-connections-explained')}
|
|
94
|
+
${connections.map(c => this.renderConnection(asset, c, properties.find(p => p.id === c.property)))}
|
|
95
|
+
${properties.length ? this.renderAddConnection(asset, properties) : html`<p hint>${i18next.t('figure.text.data-no-properties')}</p>`}
|
|
96
|
+
</section>
|
|
97
|
+
${properties.length ? this.renderSample(asset, properties) : nothing}
|
|
98
|
+
`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private renderProperty(p: FigureDataProperty) {
|
|
102
|
+
const set = (patch: Partial<FigureDataProperty>) => this.edit({ kind: 'set-data-property', property: { ...p, ...patch } as FigureDataProperty })
|
|
103
|
+
const value = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value
|
|
104
|
+
const number = (e: Event) => (e.target as HTMLInputElement).valueAsNumber
|
|
105
|
+
return html`<section item>
|
|
106
|
+
<div head>
|
|
107
|
+
<strong>${p.label} <small>(${i18next.t('figure.label.data-kind-' + p.kind)})</small></strong>
|
|
108
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-property')} @click=${() => this.edit({ kind: 'remove-data-property', id: p.id })}>
|
|
109
|
+
<md-icon>close</md-icon>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div form>
|
|
113
|
+
<span>${i18next.t('figure.label.data-id')}</span>
|
|
114
|
+
<input .value=${p.id} ?disabled=${this.locked} @change=${(e: Event) => value(e).trim() !== p.id && this.edit({ kind: 'rename-data-property', id: p.id, to: value(e).trim() })} />
|
|
115
|
+
<span>${i18next.t('figure.label.name')}</span>
|
|
116
|
+
<input .value=${p.label} ?disabled=${this.locked} @change=${(e: Event) => value(e).trim() && set({ label: value(e).trim() })} />
|
|
117
|
+
${p.kind === 'number'
|
|
118
|
+
? html`<span>${i18next.t('figure.label.data-unit')}</span>
|
|
119
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => set({ unit: value(e) as never })}>
|
|
120
|
+
${FIGURE_UNITS.map(u => html`<option value=${u} ?selected=${u === p.unit}>${u}</option>`)}
|
|
121
|
+
</select>
|
|
122
|
+
<span>${i18next.t('figure.label.data-min')}</span>
|
|
123
|
+
<input type="number" step="any" .value=${String(p.min)} ?disabled=${this.locked} @change=${(e: Event) => set({ min: number(e) })} />
|
|
124
|
+
<span>${i18next.t('figure.label.data-max')}</span>
|
|
125
|
+
<input type="number" step="any" .value=${String(p.max)} ?disabled=${this.locked} @change=${(e: Event) => set({ max: number(e) })} />
|
|
126
|
+
<span>${i18next.t('figure.label.data-default')}</span>
|
|
127
|
+
<input type="number" step="any" .value=${String(p.default)} ?disabled=${this.locked} @change=${(e: Event) => set({ default: number(e) })} />`
|
|
128
|
+
: p.kind === 'boolean'
|
|
129
|
+
? html`<span>${i18next.t('figure.label.data-default')}</span>
|
|
130
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => set({ default: value(e) === 'true' })}>
|
|
131
|
+
${[false, true].map(b => html`<option value=${String(b)} ?selected=${b === p.default}>${i18next.t(b ? 'figure.label.data-true' : 'figure.label.data-false')}</option>`)}
|
|
132
|
+
</select>`
|
|
133
|
+
: html`<span>${i18next.t('figure.label.data-values')}</span>
|
|
134
|
+
<input
|
|
135
|
+
.value=${p.values.join(', ')}
|
|
136
|
+
?disabled=${this.locked}
|
|
137
|
+
@change=${(e: Event) => {
|
|
138
|
+
const values = value(e).split(',').map(v => v.trim()).filter(Boolean)
|
|
139
|
+
set({ values, default: values.includes(p.default) ? p.default : values[0]! } as never)
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
<span>${i18next.t('figure.label.data-default')}</span>
|
|
143
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => set({ default: value(e) })}>
|
|
144
|
+
${p.values.map(v => html`<option value=${v} ?selected=${v === p.default}>${v}</option>`)}
|
|
145
|
+
</select>`}
|
|
146
|
+
</div>
|
|
147
|
+
</section>`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private renderAddProperty(asset: FigureAsset) {
|
|
151
|
+
const a = this.adding
|
|
152
|
+
return html`<div form>
|
|
153
|
+
<span>${i18next.t('figure.label.name')}</span>
|
|
154
|
+
<input .value=${a.label} @input=${(e: Event) => (this.adding = { ...a, label: (e.target as HTMLInputElement).value })} />
|
|
155
|
+
<span>${i18next.t('figure.label.data-kind')}</span>
|
|
156
|
+
<select @change=${(e: Event) => (this.adding = { ...a, kind: (e.target as HTMLSelectElement).value as FigureDataProperty['kind'] })}>
|
|
157
|
+
${(['number', 'boolean', 'enum'] as const).map(k => html`<option value=${k} ?selected=${k === a.kind}>${i18next.t('figure.label.data-kind-' + k)}</option>`)}
|
|
158
|
+
</select>
|
|
159
|
+
</div>
|
|
160
|
+
<button
|
|
161
|
+
act
|
|
162
|
+
?disabled=${this.locked || !a.label.trim()}
|
|
163
|
+
@click=${() => {
|
|
164
|
+
this.edit({ kind: 'set-data-property', property: propertyDraftOf(asset, a.label.trim(), a.kind) })
|
|
165
|
+
this.adding = { ...a, label: '' }
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-property')}
|
|
169
|
+
</button>`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private targetName(asset: FigureAsset, c: FigureDataConnection, property?: FigureDataProperty): string {
|
|
173
|
+
const choice = property ? dataTargetsOf(asset, property.kind).find(t => t.key === targetKeyOf(c.target)) : undefined
|
|
174
|
+
const name = choice?.name ?? targetKeyOf(c.target)
|
|
175
|
+
return i18next.t('figure.label.data-target-' + c.target.kind, { name })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private renderConnection(asset: FigureAsset, c: FigureDataConnection, property?: FigureDataProperty) {
|
|
179
|
+
const set = (patch: Partial<FigureDataConnection>) => this.edit({ kind: 'set-data-connection', connection: { ...c, ...patch } })
|
|
180
|
+
const number = (e: Event) => (e.target as HTMLInputElement).valueAsNumber
|
|
181
|
+
const t = c.transform
|
|
182
|
+
return html`<section item>
|
|
183
|
+
<div head>
|
|
184
|
+
<strong>${property?.label ?? c.property} → ${this.targetName(asset, c, property)}</strong>
|
|
185
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-connection')} @click=${() => this.edit({ kind: 'remove-data-connection', id: c.id })}>
|
|
186
|
+
<md-icon>close</md-icon>
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
<div form>
|
|
190
|
+
${c.target.kind === 'state'
|
|
191
|
+
? html`<span>${i18next.t('figure.label.data-source-mode')}</span>
|
|
192
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => set({ target: { ...(c.target as { kind: 'state'; input: string }), sourceMode: (e.target as HTMLSelectElement).value as 'observed' | 'commanded' } })}>
|
|
193
|
+
${(['observed', 'commanded'] as const).map(m => html`<option value=${m} ?selected=${c.target.kind === 'state' && c.target.sourceMode === m}>${i18next.t('figure.label.data-' + m)}</option>`)}
|
|
194
|
+
</select>`
|
|
195
|
+
: nothing}
|
|
196
|
+
${t.kind === 'lookup'
|
|
197
|
+
? nothing
|
|
198
|
+
: html`<span>${i18next.t('figure.label.data-transform')}</span>
|
|
199
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => set({ transform: (e.target as HTMLSelectElement).value === 'affine' ? { kind: 'affine', scale: 1, offset: 0 } : { kind: 'identity' } })}>
|
|
200
|
+
<option value="identity" ?selected=${t.kind === 'identity'}>${i18next.t('figure.label.data-identity')}</option>
|
|
201
|
+
<option value="affine" ?selected=${t.kind === 'affine'}>${i18next.t('figure.label.data-affine')}</option>
|
|
202
|
+
</select>`}
|
|
203
|
+
${t.kind === 'affine'
|
|
204
|
+
? html`<span>${i18next.t('figure.label.data-scale')}</span>
|
|
205
|
+
<input type="number" step="any" .value=${String(t.scale)} ?disabled=${this.locked} @change=${(e: Event) => set({ transform: { ...t, scale: number(e) } })} />
|
|
206
|
+
<span>${i18next.t('figure.label.data-offset')}</span>
|
|
207
|
+
<input type="number" step="any" .value=${String(t.offset)} ?disabled=${this.locked} @change=${(e: Event) => set({ transform: { ...t, offset: number(e) } })} />`
|
|
208
|
+
: nothing}
|
|
209
|
+
</div>
|
|
210
|
+
${t.kind === 'lookup' ? this.renderCases(asset, c, set) : nothing}
|
|
211
|
+
</section>`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A case per value of the property: what the target takes for it -- a number, a colour, or a clip of the group. */
|
|
215
|
+
private renderCases(asset: FigureAsset, c: FigureDataConnection, set: (patch: Partial<FigureDataConnection>) => void) {
|
|
216
|
+
if (c.transform.kind !== 'lookup') return nothing
|
|
217
|
+
const cases = c.transform.cases
|
|
218
|
+
const write = (i: number, value: FigureDataValue) => set({ transform: { kind: 'lookup', cases: cases.map((k, j) => (j === i ? { ...k, value } : k)) } })
|
|
219
|
+
const clips = c.target.kind === 'motion-clip' ? ((asset.motionGroups ?? []).find(g => c.target.kind === 'motion-clip' && g.id === c.target.group)?.clips ?? []) : []
|
|
220
|
+
return html`<div cases>
|
|
221
|
+
${cases.map(
|
|
222
|
+
(k, i) => html`<span>${typeof k.when === 'boolean' ? i18next.t(k.when ? 'figure.label.data-true' : 'figure.label.data-false') : String(k.when)}</span>
|
|
223
|
+
${c.target.kind === 'palette'
|
|
224
|
+
? html`<input type="color" .value=${String(k.value)} ?disabled=${this.locked} @change=${(e: Event) => write(i, (e.target as HTMLInputElement).value)} />`
|
|
225
|
+
: c.target.kind === 'motion-clip'
|
|
226
|
+
? html`<select ?disabled=${this.locked} @change=${(e: Event) => write(i, (e.target as HTMLSelectElement).value)}>
|
|
227
|
+
${clips.map(clip => html`<option value=${clip} ?selected=${clip === k.value}>${clip}</option>`)}
|
|
228
|
+
</select>`
|
|
229
|
+
: html`<input type="number" step="any" .value=${String(k.value)} ?disabled=${this.locked} @change=${(e: Event) => write(i, (e.target as HTMLInputElement).valueAsNumber)} />`}`
|
|
230
|
+
)}
|
|
231
|
+
</div>`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private renderAddConnection(asset: FigureAsset, properties: FigureDataProperty[]) {
|
|
235
|
+
const a = this.connecting
|
|
236
|
+
const property = properties.find(p => p.id === a.property) ?? properties[0]!
|
|
237
|
+
const targets = dataTargetsOf(asset, property.kind)
|
|
238
|
+
const choice = targets.find(t => t.key === a.target) ?? targets[0]
|
|
239
|
+
return html`<div form>
|
|
240
|
+
<span>${i18next.t('figure.label.data-property')}</span>
|
|
241
|
+
<select @change=${(e: Event) => (this.connecting = { ...a, property: (e.target as HTMLSelectElement).value })}>
|
|
242
|
+
${properties.map(p => html`<option value=${p.id} ?selected=${p === property}>${p.label}</option>`)}
|
|
243
|
+
</select>
|
|
244
|
+
<span>${i18next.t('figure.label.data-target')}</span>
|
|
245
|
+
<select ?disabled=${!targets.length} @change=${(e: Event) => (this.connecting = { ...a, target: (e.target as HTMLSelectElement).value })}>
|
|
246
|
+
${targets.map((t: DataTargetChoice) => html`<option value=${t.key} ?selected=${t === choice}>${i18next.t('figure.label.data-target-' + t.target.kind, { name: t.name })}</option>`)}
|
|
247
|
+
</select>
|
|
248
|
+
</div>
|
|
249
|
+
<button act ?disabled=${this.locked || !choice} @click=${() => this.edit({ kind: 'set-data-connection', connection: connectionDraftOf(asset, property, choice!) })}>
|
|
250
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-connection')}
|
|
251
|
+
</button>`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Values to try: what they move, by figure-model's evaluation; in the preview, the figure running on them. */
|
|
255
|
+
private renderSample(asset: FigureAsset, properties: FigureDataProperty[]) {
|
|
256
|
+
const values = Object.fromEntries(properties.map(p => [p.id, this.sample[p.id] ?? p.default]))
|
|
257
|
+
const outcome = dataSampleOf(asset, values)
|
|
258
|
+
return html`<section>
|
|
259
|
+
<h3>${i18next.t('figure.label.data-sample')}${this.help.button('figure.text.data-sample-explained')}</h3>
|
|
260
|
+
${this.help.hint('figure.text.data-sample-explained')}
|
|
261
|
+
<div form>
|
|
262
|
+
${properties.map(
|
|
263
|
+
p => html`<span>${p.label}</span>
|
|
264
|
+
${p.kind === 'number'
|
|
265
|
+
? html`<input type="number" step="any" .value=${String(values[p.id])} @change=${(e: Event) => this.try(p, (e.target as HTMLInputElement).value)} />`
|
|
266
|
+
: html`<select @change=${(e: Event) => this.try(p, (e.target as HTMLSelectElement).value)}>
|
|
267
|
+
${domainOf(p).map(v => html`<option value=${String(v)} ?selected=${v === values[p.id]}>${typeof v === 'boolean' ? i18next.t(v ? 'figure.label.data-true' : 'figure.label.data-false') : v}</option>`)}
|
|
268
|
+
</select>`}`
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
${'issues' in outcome
|
|
272
|
+
? html`<p hint>${outcome.issues.map(i => i18next.t('figure.text.data-value-' + i.reason, { property: i.property ?? '' })).join(' · ')}</p>`
|
|
273
|
+
: html`<ul result>
|
|
274
|
+
${Object.entries(outcome.result.states).map(([input, s]) => html`<li>${asset.stateInputs?.[input]?.label ?? input} = ${s.value}</li>`)}
|
|
275
|
+
${Object.entries(outcome.result.palette).map(([token, colour]) => html`<li>${token} = ${colour}</li>`)}
|
|
276
|
+
${Object.entries(outcome.result.motion).map(
|
|
277
|
+
([group, m]) => html`<li>${(asset.motionGroups ?? []).find(g => g.id === group)?.label ?? group}: ${[m.clip, m.speed === undefined ? '' : `× ${m.speed}`].filter(Boolean).join(' ')}</li>`
|
|
278
|
+
)}
|
|
279
|
+
</ul>`}
|
|
280
|
+
<label toggle>
|
|
281
|
+
<input
|
|
282
|
+
type="checkbox"
|
|
283
|
+
.checked=${this.sampling}
|
|
284
|
+
@change=${(e: Event) => {
|
|
285
|
+
this.sampling = (e.target as HTMLInputElement).checked
|
|
286
|
+
this.tell(values)
|
|
287
|
+
}}
|
|
288
|
+
/>
|
|
289
|
+
${i18next.t('figure.label.data-sample-in-preview')}
|
|
290
|
+
</label>
|
|
291
|
+
</section>`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private try(property: FigureDataProperty, typed: string) {
|
|
295
|
+
this.sample = { ...this.sample, [property.id]: sampleValueOf(property, typed) }
|
|
296
|
+
const properties = this.asset?.dataInterface?.properties ?? []
|
|
297
|
+
this.tell(Object.fromEntries(properties.map(p => [p.id, this.sample[p.id] ?? p.default])))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The sample to the preview while it is asked for; null lets the preview's figure go back to its own values. */
|
|
301
|
+
private tell(values: Record<string, FigureDataValue>) {
|
|
302
|
+
this.dispatchEvent(new CustomEvent('data-sample', { detail: { values: this.sampling ? values : null }, bubbles: true, composed: true }))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private edit(edit: unknown) {
|
|
306
|
+
this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit }, bubbles: true, composed: true }))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -196,6 +196,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
196
196
|
/** Clip speeds and control values given here -- the preview's, not the figure's. */
|
|
197
197
|
@state() private driving: Record<string, number> = {}
|
|
198
198
|
@state() private giving: Record<string, number> = {}
|
|
199
|
+
/** Per motion group, the clip played here and how fast (ADR-0051 §3.2) -- the preview's, as a board's \`motion\`. */
|
|
200
|
+
@state() private moving: Record<string, { clip: string; speed: number }> = {}
|
|
201
|
+
/** Data values tried in the data tab: the preview's figures run on them as a live snapshot; null when none are. */
|
|
202
|
+
@property({ attribute: false }) sample: Record<string, unknown> | null = null
|
|
199
203
|
/** The part chosen in the editor. The size help starts on it, so the help speaks of the part the author was working on. */
|
|
200
204
|
@property() selected = ''
|
|
201
205
|
@state() private setupPart = ''
|
|
@@ -215,7 +219,9 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
215
219
|
render() {
|
|
216
220
|
const asset = this.asset
|
|
217
221
|
const controls = asset ? asset.document.model.inputs.filter(i => i.role === 'state') : []
|
|
218
|
-
|
|
222
|
+
/* Clips in a motion group are played by the group's choice of clip, not each by its own speed. */
|
|
223
|
+
const grouped = new Set((asset?.motionGroups ?? []).flatMap(g => g.clips))
|
|
224
|
+
const clips = asset ? [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c)) : []
|
|
219
225
|
let clampsByWay: ReturnType<typeof previewClampsOf> = {}
|
|
220
226
|
try {
|
|
221
227
|
clampsByWay = asset ? previewClampsOf(asset, this.authoredSize()) : {}
|
|
@@ -245,7 +251,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
245
251
|
</select>
|
|
246
252
|
<button @click=${() => this.dispatchEvent(new CustomEvent('configure-size', { detail: { part: setupPart }, bubbles: true, composed: true }))}>${i18next.t('figure.button.configure-size')}</button>
|
|
247
253
|
</div>` : nothing}
|
|
248
|
-
${controls.length + clips.length ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
|
|
254
|
+
${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
private renderDrivers(asset: FigureAsset, controls: FigureAsset['document']['model']['inputs'], clips: string[]) {
|
|
@@ -287,9 +293,25 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
287
293
|
<output>${value.toFixed(2)}</output>
|
|
288
294
|
</label>`
|
|
289
295
|
})}
|
|
296
|
+
${(asset.motionGroups ?? []).map(group => {
|
|
297
|
+
const playing = this.moving[group.id] ?? { clip: group.defaultClip, speed: 0 }
|
|
298
|
+
return html`<label>
|
|
299
|
+
<span>${group.label} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
|
|
300
|
+
<select aria-label=${group.label} @change=${(e: Event) => this.move(group.id, { ...playing, clip: (e.target as HTMLSelectElement).value })}>
|
|
301
|
+
${group.clips.map(c => html`<option value=${c} ?selected=${c === playing.clip}>${c}</option>`)}
|
|
302
|
+
</select>
|
|
303
|
+
<input type="range" min="0" max="3" step="0.05" .value=${String(playing.speed)} @input=${(e: Event) => this.move(group.id, { ...playing, speed: Number((e.target as HTMLInputElement).value) })} />
|
|
304
|
+
<output>${playing.speed.toFixed(2)}</output>
|
|
305
|
+
</label>`
|
|
306
|
+
})}
|
|
290
307
|
</div>`
|
|
291
308
|
}
|
|
292
309
|
|
|
310
|
+
private move(group: string, playing: { clip: string; speed: number }) {
|
|
311
|
+
this.moving = { ...this.moving, [group]: playing }
|
|
312
|
+
this.tellInstances('motion', Object.fromEntries(Object.entries(this.moving).map(([id, m]) => [id, { ...m, paused: false }])))
|
|
313
|
+
}
|
|
314
|
+
|
|
293
315
|
private renderDial(control: { unit: string; min: number; max: number }, value: number) {
|
|
294
316
|
if (control.unit !== 'deg') return html`<span></span>`
|
|
295
317
|
const dial = dialShape(control.min, control.max, value)
|
|
@@ -316,11 +338,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
316
338
|
this.giving = {}
|
|
317
339
|
}
|
|
318
340
|
|
|
319
|
-
private tellInstances(key: 'clips' | 'parameters', values: Record<string,
|
|
320
|
-
for (const component of this.scene?.root?.components ?? [])
|
|
341
|
+
private tellInstances(key: 'clips' | 'parameters' | 'motion' | 'figureData', values: Record<string, unknown> | null) {
|
|
342
|
+
for (const component of this.scene?.root?.components ?? []) {
|
|
343
|
+
const value = values === null ? null : { ...values }
|
|
344
|
+
/* A live snapshot is transient on the figure (its \`figureData\` accessor), never written into the saved model. */
|
|
345
|
+
if (key === 'figureData') (component as { figureData?: unknown }).figureData = value
|
|
346
|
+
else component.set?.(key, value)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
/** The data sample to the figures standing now: a live snapshot, transient on each, never saved with the board. */
|
|
352
|
+
private tellSample() {
|
|
353
|
+
if (this.sample) this.tellInstances('figureData', { ...this.sample })
|
|
321
354
|
}
|
|
322
355
|
|
|
323
356
|
willUpdate(changed: Map<string, unknown>) {
|
|
357
|
+
if (changed.has('sample') && this.scene) this.tellInstances('figureData', this.sample ? { ...this.sample } : null)
|
|
324
358
|
if (!this.hasUpdated) return
|
|
325
359
|
if (!this.active) {
|
|
326
360
|
this.stale = true
|
|
@@ -387,6 +421,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
387
421
|
const again = this.scene?.rootContainer?.rebuildContentModel && this.registered === type
|
|
388
422
|
if (!again) this.teardown()
|
|
389
423
|
this.driving = {}
|
|
424
|
+
this.moving = {}
|
|
390
425
|
registerFigure(type, asset)
|
|
391
426
|
this.registered = type
|
|
392
427
|
const { components, board } = previewInstances(type, size)
|
|
@@ -408,6 +443,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
408
443
|
this.scene!.rootContainer!.rebuildContentModel!(model, { preserveThreeD: true })
|
|
409
444
|
this.standing = signature
|
|
410
445
|
this.cannot = ''
|
|
446
|
+
this.tellSample()
|
|
411
447
|
return
|
|
412
448
|
} catch {
|
|
413
449
|
this.teardown()
|
|
@@ -425,6 +461,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
425
461
|
this.scene.fit?.('ratio')
|
|
426
462
|
this.scene.root?.fitCameraToModel?.()
|
|
427
463
|
this.cannot = ''
|
|
464
|
+
this.tellSample()
|
|
428
465
|
} catch (error) {
|
|
429
466
|
this.cannot = (error as Error).message
|
|
430
467
|
}
|
|
@@ -237,10 +237,56 @@ export function editRefusalOf(error: unknown, say: Say): string {
|
|
|
237
237
|
documentLimitRefusalOf(error, say) ??
|
|
238
238
|
sectionRefusalOf(error, say) ??
|
|
239
239
|
slotLayoutRefusalOf(error, say) ??
|
|
240
|
+
dataMotionRefusalOf(error, say) ??
|
|
240
241
|
say('figure.text.edit-refused', { code: codeOf(error) ?? '' })
|
|
241
242
|
)
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
/**
|
|
246
|
+
* A data property, a connection, a motion group or a clip's tracks refused (ADR-0051 §2.2, §3.2), said with the ids and
|
|
247
|
+
* numbers the kernel gave. Nothing for any other refusal.
|
|
248
|
+
*/
|
|
249
|
+
export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
|
|
250
|
+
const invalidId = detailsOf(error, 'data-id-invalid')
|
|
251
|
+
if (invalidId) return say('figure.text.data-id-invalid', { id: invalidId.id })
|
|
252
|
+
const twice = detailsOf(error, 'data-id-duplicate')
|
|
253
|
+
if (twice) return say('figure.text.data-id-duplicate', { id: twice.id })
|
|
254
|
+
const property = detailsOf(error, 'data-property-invalid')
|
|
255
|
+
if (property) return say('figure.text.data-property-invalid-' + property.field, { property: property.property })
|
|
256
|
+
const absent = detailsOf(error, 'data-property-absent')
|
|
257
|
+
if (absent) return say('figure.text.data-property-absent', { property: absent.property })
|
|
258
|
+
const target = detailsOf(error, 'data-target-absent')
|
|
259
|
+
if (target) return say('figure.text.data-target-absent-' + target.target, { id: target.id })
|
|
260
|
+
const writers = detailsOf(error, 'data-target-duplicate')
|
|
261
|
+
if (writers) return say('figure.text.data-target-duplicate', { a: writers.connections[0], b: writers.connections[1], id: writers.id })
|
|
262
|
+
const domain = detailsOf(error, 'data-transform-domain')
|
|
263
|
+
if (domain)
|
|
264
|
+
return say('figure.text.data-transform-' + domain.why, {
|
|
265
|
+
from: domain.from ?? '',
|
|
266
|
+
to: domain.to ?? '',
|
|
267
|
+
min: domain.min === undefined ? '' : numberOf(domain.min),
|
|
268
|
+
max: domain.max === undefined ? '' : numberOf(domain.max),
|
|
269
|
+
low: domain.within ? numberOf(domain.within[0]) : '',
|
|
270
|
+
high: domain.within ? numberOf(domain.within[1]) : '',
|
|
271
|
+
value: domain.value === undefined ? '' : String(domain.value)
|
|
272
|
+
})
|
|
273
|
+
const connected = detailsOf(error, 'data-property-connected')
|
|
274
|
+
if (connected) return say('figure.text.data-property-connected', { property: connected.property, connections: connected.connections.join(', ') })
|
|
275
|
+
const clipAbsent = detailsOf(error, 'motion-clip-absent')
|
|
276
|
+
if (clipAbsent) return say('figure.text.motion-clip-absent', { clip: clipAbsent.clip, group: clipAbsent.group })
|
|
277
|
+
const shared = detailsOf(error, 'motion-clip-shared')
|
|
278
|
+
if (shared) return say('figure.text.motion-clip-shared', { clip: shared.clip, group: shared.groups[0] })
|
|
279
|
+
const many = detailsOf(error, 'driver-writers')
|
|
280
|
+
if (many) return say('figure.text.driver-writers', { state: many.state, clips: many.clips.join(', ') })
|
|
281
|
+
const which = detailsOf(error, 'driver-clip-required')
|
|
282
|
+
if (which) return say('figure.text.driver-clip-required', { state: which.state, clips: which.clips.join(', ') })
|
|
283
|
+
const setting = detailsOf(error, 'motion-setting')
|
|
284
|
+
if (setting) return say('figure.text.motion-setting-' + setting.why, { group: setting.group, clip: setting.clip ?? '' })
|
|
285
|
+
const key = codeOf(error) === 'MOTION_GROUP' ? detailsOf(error, 'value-outside-range') : undefined
|
|
286
|
+
if (key) return say('figure.text.motion-key-outside-range', { input: key.input, value: numberOf(key.value), min: numberOf(key.min), max: numberOf(key.max) })
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
244
290
|
/**
|
|
245
291
|
* A value outside the range its input declares, with the range and the value given. The figure's own size is named by
|
|
246
292
|
* its axis; any other input by its range only.
|
|
@@ -20,8 +20,38 @@ export interface Clip {
|
|
|
20
20
|
tracks: ClipTrack[];
|
|
21
21
|
}
|
|
22
22
|
export declare function clipsOf(asset: FigureAsset): Clip[];
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays
|
|
25
|
+
* outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip
|
|
26
|
+
* plays. Without a clip, the controls no clip plays at all.
|
|
27
|
+
*/
|
|
28
|
+
export declare function undrivenOf(asset: FigureAsset, clip?: string, group?: string): string[];
|
|
29
|
+
/** A motion group as the panel shows it: its setting, and the clips it holds in the clips' order. */
|
|
30
|
+
export interface ClipGroup {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
clips: string[];
|
|
34
|
+
defaultClip: string;
|
|
35
|
+
transitionSeconds: number;
|
|
36
|
+
}
|
|
37
|
+
/** The figure's motion groups, and the clips in none -- the ones a group can take. */
|
|
38
|
+
export declare function clipGroupsOf(asset: FigureAsset): {
|
|
39
|
+
groups: ClipGroup[];
|
|
40
|
+
free: string[];
|
|
41
|
+
};
|
|
42
|
+
/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */
|
|
43
|
+
export declare function freeGroupIdOf(asset: FigureAsset): string;
|
|
44
|
+
/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */
|
|
45
|
+
export declare const groupEdit: (group: ClipGroup, patch?: Partial<Omit<ClipGroup, "id">>) => {
|
|
46
|
+
kind: "set-motion-group";
|
|
47
|
+
group: {
|
|
48
|
+
id: string;
|
|
49
|
+
label: string;
|
|
50
|
+
clips: string[];
|
|
51
|
+
defaultClip: string;
|
|
52
|
+
transitionSeconds: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
25
55
|
/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */
|
|
26
56
|
export declare function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[];
|
|
27
57
|
/** A key added at a moment, replacing one already there; the keys stay in time order. */
|
|
@@ -21,11 +21,39 @@ export function clipsOf(asset) {
|
|
|
21
21
|
}
|
|
22
22
|
return [...clips.values()];
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays
|
|
26
|
+
* outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip
|
|
27
|
+
* plays. Without a clip, the controls no clip plays at all.
|
|
28
|
+
*/
|
|
29
|
+
export function undrivenOf(asset, clip = '', group) {
|
|
30
|
+
const groupOf = new Map((asset.motionGroups ?? []).flatMap(g => g.clips.map(c => [c, g.id])));
|
|
31
|
+
const mine = group ?? (clip ? groupOf.get(clip) : undefined);
|
|
32
|
+
const taken = (state) => (asset.drivers ?? []).some(d => d.state === state && (d.clip === clip || mine === undefined || groupOf.get(d.clip) !== mine));
|
|
33
|
+
return asset.document.model.inputs.filter(i => i.role === 'state' && !taken(i.id)).map(i => i.id);
|
|
28
34
|
}
|
|
35
|
+
/** The figure's motion groups, and the clips in none -- the ones a group can take. */
|
|
36
|
+
export function clipGroupsOf(asset) {
|
|
37
|
+
const groups = (asset.motionGroups ?? []).map(g => ({ ...g, clips: [...g.clips] }));
|
|
38
|
+
const grouped = new Set(groups.flatMap(g => g.clips));
|
|
39
|
+
const free = [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c));
|
|
40
|
+
return { groups, free };
|
|
41
|
+
}
|
|
42
|
+
/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */
|
|
43
|
+
export function freeGroupIdOf(asset) {
|
|
44
|
+
const taken = new Set((asset.motionGroups ?? []).map(g => g.id));
|
|
45
|
+
if (!taken.has('motion'))
|
|
46
|
+
return 'motion';
|
|
47
|
+
let n = 2;
|
|
48
|
+
while (taken.has(`motion-${n}`))
|
|
49
|
+
n++;
|
|
50
|
+
return `motion-${n}`;
|
|
51
|
+
}
|
|
52
|
+
/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */
|
|
53
|
+
export const groupEdit = (group, patch = {}) => ({
|
|
54
|
+
kind: 'set-motion-group',
|
|
55
|
+
group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }
|
|
56
|
+
});
|
|
29
57
|
/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */
|
|
30
58
|
export function poseAt(asset, clip, seconds) {
|
|
31
59
|
const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clips.js","sourceRoot":"","sources":["../../client/modeller/clips.ts"],"names":[],"mappings":"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","sourcesContent":["import { driverValue } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureDriver, FigureInputChange } from '@hatiolab/figure-model'\n\n/*\n * The figure's clips: controls played by keys over time, grouped into clips that each run on one\n * clock. Kept apart from the element so the reading is tested without a browser.\n */\nexport interface ClipKey {\n seconds: number\n value: number\n}\nexport interface ClipTrack {\n state: string\n label: string\n unit: string\n min: number\n max: number\n keys: ClipKey[]\n interpolation: 'linear' | 'step'\n loop: 'wrap' | 'hold'\n accumulates: boolean\n}\nexport interface Clip {\n name: string\n duration: number\n tracks: ClipTrack[]\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\nexport function clipsOf(asset: FigureAsset): Clip[] {\n const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]))\n const clips = new Map<string, Clip>()\n for (const d of asset.drivers ?? []) {\n const input = inputs.get(d.state)\n const clip = clips.get(d.clip) ?? { name: d.clip, duration: d.time.duration, tracks: [] }\n clip.tracks.push({\n state: d.state,\n label: asset.stateInputs?.[d.state]?.label ?? d.state,\n unit: input?.unit ?? '',\n min: input?.min ?? 0,\n max: input?.max ?? 0,\n keys: d.keys.map(k => ({ seconds: round(k.at * d.time.duration), value: k.value })),\n interpolation: d.interpolation,\n loop: d.loop,\n accumulates: d.accumulates\n })\n clips.set(d.clip, clip)\n }\n return [...clips.values()]\n}\n\n/** The controls no clip plays yet -- the ones a new track can take. One driver per control. */\nexport function undrivenOf(asset: FigureAsset): string[] {\n const driven = new Set((asset.drivers ?? []).map(d => d.state))\n return asset.document.model.inputs.filter(i => i.role === 'state' && !driven.has(i.id)).map(i => i.id)\n}\n\n/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */\nexport function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {\n const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))\n return (asset.drivers ?? [])\n .filter((d: FigureDriver) => d.clip === clip)\n .map(d => {\n const unit = units.get(d.state)!\n return { kind: 'set-state' as const, input: d.state, unit, value: driverValue(d, seconds, unit === 'deg' || unit === 'rad' ? unit : 'other') }\n })\n}\n\n/** A key added at a moment, replacing one already there; the keys stay in time order. */\nexport function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[] {\n return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds)\n}\n\n/** A track's keys without one; a track keeps at least its two ends, as the format asks. */\nexport function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null {\n if (keys.length <= 2 || index === 0) return null\n return keys.filter((_, i) => i !== index)\n}\n\n/** The edit that writes a track back. */\nexport const trackEdit = (clip: Clip, track: ClipTrack, patch: Partial<Pick<ClipTrack, 'keys' | 'loop' | 'interpolation'>> = {}) => ({\n kind: 'set-driver' as const,\n state: track.state,\n clip: clip.name,\n keys: patch.keys ?? track.keys,\n loop: patch.loop ?? track.loop,\n interpolation: patch.interpolation ?? track.interpolation\n})\n"]}
|
|
1
|
+
{"version":3,"file":"clips.js","sourceRoot":"","sources":["../../client/modeller/clips.ts"],"names":[],"mappings":"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","sourcesContent":["import { driverValue } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureDriver, FigureInputChange } from '@hatiolab/figure-model'\n\n/*\n * The figure's clips: controls played by keys over time, grouped into clips that each run on one\n * clock. Kept apart from the element so the reading is tested without a browser.\n */\nexport interface ClipKey {\n seconds: number\n value: number\n}\nexport interface ClipTrack {\n state: string\n label: string\n unit: string\n min: number\n max: number\n keys: ClipKey[]\n interpolation: 'linear' | 'step'\n loop: 'wrap' | 'hold'\n accumulates: boolean\n}\nexport interface Clip {\n name: string\n duration: number\n tracks: ClipTrack[]\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\nexport function clipsOf(asset: FigureAsset): Clip[] {\n const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]))\n const clips = new Map<string, Clip>()\n for (const d of asset.drivers ?? []) {\n const input = inputs.get(d.state)\n const clip = clips.get(d.clip) ?? { name: d.clip, duration: d.time.duration, tracks: [] }\n clip.tracks.push({\n state: d.state,\n label: asset.stateInputs?.[d.state]?.label ?? d.state,\n unit: input?.unit ?? '',\n min: input?.min ?? 0,\n max: input?.max ?? 0,\n keys: d.keys.map(k => ({ seconds: round(k.at * d.time.duration), value: k.value })),\n interpolation: d.interpolation,\n loop: d.loop,\n accumulates: d.accumulates\n })\n clips.set(d.clip, clip)\n }\n return [...clips.values()]\n}\n\n/**\n * The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays\n * outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip\n * plays. Without a clip, the controls no clip plays at all.\n */\nexport function undrivenOf(asset: FigureAsset, clip = '', group?: string): string[] {\n const groupOf = new Map((asset.motionGroups ?? []).flatMap(g => g.clips.map(c => [c, g.id] as const)))\n const mine = group ?? (clip ? groupOf.get(clip) : undefined)\n const taken = (state: string) =>\n (asset.drivers ?? []).some(d => d.state === state && (d.clip === clip || mine === undefined || groupOf.get(d.clip) !== mine))\n return asset.document.model.inputs.filter(i => i.role === 'state' && !taken(i.id)).map(i => i.id)\n}\n\n/** A motion group as the panel shows it: its setting, and the clips it holds in the clips' order. */\nexport interface ClipGroup {\n id: string\n label: string\n clips: string[]\n defaultClip: string\n transitionSeconds: number\n}\n\n/** The figure's motion groups, and the clips in none -- the ones a group can take. */\nexport function clipGroupsOf(asset: FigureAsset): { groups: ClipGroup[]; free: string[] } {\n const groups = (asset.motionGroups ?? []).map(g => ({ ...g, clips: [...g.clips] }))\n const grouped = new Set(groups.flatMap(g => g.clips))\n const free = [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c))\n return { groups, free }\n}\n\n/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */\nexport function freeGroupIdOf(asset: FigureAsset): string {\n const taken = new Set((asset.motionGroups ?? []).map(g => g.id))\n if (!taken.has('motion')) return 'motion'\n let n = 2\n while (taken.has(`motion-${n}`)) n++\n return `motion-${n}`\n}\n\n/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */\nexport const groupEdit = (group: ClipGroup, patch: Partial<Omit<ClipGroup, 'id'>> = {}) => ({\n kind: 'set-motion-group' as const,\n group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }\n})\n\n/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */\nexport function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {\n const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))\n return (asset.drivers ?? [])\n .filter((d: FigureDriver) => d.clip === clip)\n .map(d => {\n const unit = units.get(d.state)!\n return { kind: 'set-state' as const, input: d.state, unit, value: driverValue(d, seconds, unit === 'deg' || unit === 'rad' ? unit : 'other') }\n })\n}\n\n/** A key added at a moment, replacing one already there; the keys stay in time order. */\nexport function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[] {\n return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds)\n}\n\n/** A track's keys without one; a track keeps at least its two ends, as the format asks. */\nexport function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null {\n if (keys.length <= 2 || index === 0) return null\n return keys.filter((_, i) => i !== index)\n}\n\n/** The edit that writes a track back. */\nexport const trackEdit = (clip: Clip, track: ClipTrack, patch: Partial<Pick<ClipTrack, 'keys' | 'loop' | 'interpolation'>> = {}) => ({\n kind: 'set-driver' as const,\n state: track.state,\n clip: clip.name,\n keys: patch.keys ?? track.keys,\n loop: patch.loop ?? track.loop,\n interpolation: patch.interpolation ?? track.interpolation\n})\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FigureAsset, FigureDataConnection, FigureDataProperty, FigureDataResult, FigureDataTarget, FigureDataValue, FigureDataValueIssue } from '@hatiolab/figure-model';
|
|
2
|
+
/** Something a connection can write, with what the screen shows of it. */
|
|
3
|
+
export interface DataTargetChoice {
|
|
4
|
+
/** One string per target, for a select's value. */
|
|
5
|
+
key: string;
|
|
6
|
+
target: FigureDataTarget;
|
|
7
|
+
/** The control's label, the colour token, or the group's label. */
|
|
8
|
+
name: string;
|
|
9
|
+
unit?: string;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const targetKeyOf: (t: FigureDataTarget) => string;
|
|
14
|
+
/**
|
|
15
|
+
* What a property of this kind can be connected to: a number moves a control or a group's speed; an enum or a boolean
|
|
16
|
+
* moves a control, a colour or which clip of a group plays (through a case per value). A control is offered observed: a
|
|
17
|
+
* value read from the machine applies as it comes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function dataTargetsOf(asset: FigureAsset, kind: FigureDataProperty['kind']): DataTargetChoice[];
|
|
20
|
+
/** The values a lookup has a case for: an enum's values, or both booleans. Nothing for a number. */
|
|
21
|
+
export declare const domainOf: (property: FigureDataProperty) => FigureDataValue[];
|
|
22
|
+
/** An id not taken among `taken`: `base`, then `base-2`, `base-3`, … made into the kernel's id shape. */
|
|
23
|
+
export declare function freeDataIdOf(taken: Iterable<string>, base: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* A first connection of a property to a target, for the author to adjust: a number as it is in the control's own unit,
|
|
26
|
+
* or scaled from another; an enum or a boolean case by case, each case at what the target shows now -- the control's
|
|
27
|
+
* rest value, the colour, the group's first clip. The kernel says whether it holds.
|
|
28
|
+
*/
|
|
29
|
+
export declare function connectionDraftOf(asset: FigureAsset, property: FigureDataProperty, choice: DataTargetChoice): FigureDataConnection;
|
|
30
|
+
/** What a property looks like when first made: a number 0 to 1, a boolean off, an enum of two values. */
|
|
31
|
+
export declare function propertyDraftOf(asset: FigureAsset, label: string, kind: FigureDataProperty['kind']): FigureDataProperty;
|
|
32
|
+
/** A value typed in a sample field, as the property takes it: a number, true or false, or the enum value. */
|
|
33
|
+
export declare function sampleValueOf(property: FigureDataProperty, typed: string): FigureDataValue;
|
|
34
|
+
/**
|
|
35
|
+
* What a sample of values would move, as the figure would run it: the kernel's check and evaluation of the interface.
|
|
36
|
+
* Refused whole when one value cannot be taken, with each one's reason, as a live snapshot is.
|
|
37
|
+
*/
|
|
38
|
+
export declare function dataSampleOf(asset: FigureAsset, values: Record<string, FigureDataValue>): {
|
|
39
|
+
result: FigureDataResult;
|
|
40
|
+
} | {
|
|
41
|
+
issues: FigureDataValueIssue[];
|
|
42
|
+
};
|