@hatiolab/figure-model 0.11.1 → 0.12.0
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/dist/asset-types.d.ts +65 -39
- package/dist/asset-types.d.ts.map +1 -1
- package/dist/asset-types.js.map +1 -1
- package/dist/authoring-actions.js +4 -1
- package/dist/authoring-actions.js.map +1 -1
- package/dist/authoring-bindings.js +6 -7
- package/dist/authoring-bindings.js.map +1 -1
- package/dist/authoring-dimensions.d.ts +7 -0
- package/dist/authoring-dimensions.d.ts.map +1 -1
- package/dist/authoring-dimensions.js +101 -1
- package/dist/authoring-dimensions.js.map +1 -1
- package/dist/authoring-types.d.ts +34 -4
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/capabilities.js +37 -17
- package/dist/capabilities.js.map +1 -1
- package/dist/contract.d.ts +20 -19
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/data-interface.d.ts +8 -4
- package/dist/data-interface.d.ts.map +1 -1
- package/dist/data-interface.js +251 -209
- package/dist/data-interface.js.map +1 -1
- package/dist/follow.d.ts +9 -0
- package/dist/follow.d.ts.map +1 -1
- package/dist/follow.js +9 -0
- package/dist/follow.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/item-notes.d.ts +2 -1
- package/dist/item-notes.d.ts.map +1 -1
- package/dist/item-notes.js +4 -3
- package/dist/item-notes.js.map +1 -1
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.js +1 -1
- package/dist/part-edit.d.ts +13 -12
- package/dist/part-edit.d.ts.map +1 -1
- package/dist/part-edit.js +42 -36
- package/dist/part-edit.js.map +1 -1
- package/dist/proposal.d.ts +2 -2
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +16 -13
- package/dist/proposal.js.map +1 -1
- package/dist/recipe-from-parts.d.ts.map +1 -1
- package/dist/recipe-from-parts.js +7 -0
- package/dist/recipe-from-parts.js.map +1 -1
- package/dist/release-contract.d.ts +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +41 -41
- package/dist/release-contract.js.map +1 -1
- package/dist/render-batches.d.ts +11 -1
- package/dist/render-batches.d.ts.map +1 -1
- package/dist/render-batches.js +31 -4
- package/dist/render-batches.js.map +1 -1
- package/dist/robots.js +3 -3
- package/dist/robots.js.map +1 -1
- package/dist/slot-layout-presets.d.ts +2 -2
- package/dist/slot-layout-presets.js +6 -6
- package/dist/slot-layout-presets.js.map +1 -1
- package/dist/slot-layout.d.ts +2 -2
- package/dist/slot-layout.d.ts.map +1 -1
- package/dist/slot-places.d.ts +2 -1
- package/dist/slot-places.d.ts.map +1 -1
- package/dist/slot-places.js +1 -1
- package/dist/slot-places.js.map +1 -1
- package/dist/words.d.ts +10 -0
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +10 -0
- package/dist/words.js.map +1 -1
- package/package.json +1 -1
package/dist/data-interface.js
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* A figure's data and what it moves inside the figure (ADR-0051 §8, data binding design appendix A).
|
|
6
6
|
*
|
|
7
7
|
* The author describes the data the figure takes in the site's own field names -- objects, lists, numbers, booleans,
|
|
8
|
-
* text -- and binds values of it to what the figure does
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* text -- and binds values of it to what the figure does. A binding has the board's mapping shape -- an accessor into
|
|
9
|
+
* the data, a rule and what the rule needs -- so a person who binds data on a board binds it on a figure in the same
|
|
10
|
+
* words. One binding, three rules (ADR-0166): the value as it is or scaled moves one behavior (a state input, a colour,
|
|
11
|
+
* a motion group's clip, speed or pause, the instance's position, the items in its places); a table gives one look per
|
|
12
|
+
* code or per interval to several targets at once -- parts' colour and effect, state inputs' values, a motion group.
|
|
13
|
+
* The figure's bindings are its own: every board that places it moves it with the same data, and a board rarely binds
|
|
14
|
+
* more. A table a consumer may swap for a legend says so by a role; the legend is the consumer's.
|
|
13
15
|
*
|
|
14
16
|
* This module checks the declaration once, when the asset is read: each accessor goes down the schema to what its
|
|
15
|
-
* behavior reads, and each rule's results are what
|
|
17
|
+
* behavior reads, and each rule's results are what its targets take over the whole of the value's domain. While the
|
|
16
18
|
* figure runs it checks the data against the schema -- a value of the wrong kind refuses the whole of it -- and gives
|
|
17
19
|
* each binding the value it reads. Turning that value into the behavior's input by the rule is things-scene's, the same
|
|
18
20
|
* evaluator a board mapping runs; nothing here holds time or an instance.
|
|
@@ -161,12 +163,32 @@ const targetsOf = (asset) => ({
|
|
|
161
163
|
states: new Map(asset.document.model.inputs.filter(i => i.role === 'state').map(i => [i.id, i])),
|
|
162
164
|
palette: asset.palette,
|
|
163
165
|
groups: new Map((asset.motionGroups ?? []).map(g => [g.id, g.clips])),
|
|
164
|
-
ports: new Map((asset.dataInterface?.ports ?? []).map(p => [p.id, p])),
|
|
165
166
|
parts: new Set(asset.appearance.map(a => a.target))
|
|
166
167
|
});
|
|
167
|
-
/** The one thing a binding writes, as a key no other binding may hold. Items are stacked, so several may fill them. */
|
|
168
|
-
const writeKey = (b) => b.kind === 'state' ? `state ${b.input}` : b.kind === 'palette' ? `palette ${b.token}` : b.kind === '
|
|
169
|
-
const behaviorId = (b) => b.kind === 'state' ? b.input : b.kind === 'palette' ? b.token : b.kind === '
|
|
168
|
+
/** The one thing a value binding writes, as a key no other binding may hold. Items are stacked, so several may fill them. */
|
|
169
|
+
const writeKey = (b) => b.kind === 'state' ? `state ${b.input}` : b.kind === 'palette' ? `palette ${b.token}` : b.kind === 'pose' ? `pose ${b.axis}` : b.kind === 'items' ? null : `${b.kind} ${b.group}`;
|
|
170
|
+
const behaviorId = (b) => b.kind === 'state' ? b.input : b.kind === 'palette' ? b.token : b.kind === 'pose' ? b.axis : b.kind === 'items' ? (b.slot ?? '') : b.group;
|
|
171
|
+
/** The columns of a table's looks that set a motion group, with the behavior that sets the same in a value binding. */
|
|
172
|
+
const GROUP_COLUMNS = [
|
|
173
|
+
['clip', 'motion-clip'],
|
|
174
|
+
['speed', 'motion-speed'],
|
|
175
|
+
['paused', 'motion-paused']
|
|
176
|
+
];
|
|
177
|
+
/**
|
|
178
|
+
* What a table binding writes, under the keys a value binding's behavior holds (ADR-0166 6): each of its state inputs,
|
|
179
|
+
* and each of its group's clip, speed and pause a look sets. A part's colour and effect are no one binding's alone.
|
|
180
|
+
*/
|
|
181
|
+
function tableWrites(b) {
|
|
182
|
+
const out = (b.targets.inputs ?? []).map(input => ({ key: `state ${input}`, behavior: 'state', id: input }));
|
|
183
|
+
const group = b.targets.group;
|
|
184
|
+
if (group !== undefined) {
|
|
185
|
+
const looks = Object.values(b.table);
|
|
186
|
+
for (const [column, behavior] of GROUP_COLUMNS)
|
|
187
|
+
if (looks.some(look => look[column] !== undefined))
|
|
188
|
+
out.push({ key: `${behavior} ${group}`, behavior, id: group });
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
170
192
|
function checkBehavior(b, path) {
|
|
171
193
|
if (!b || typeof b !== 'object' || Array.isArray(b))
|
|
172
194
|
fail('ASSET_SCHEMA', path, 'object required');
|
|
@@ -177,21 +199,19 @@ function checkBehavior(b, path) {
|
|
|
177
199
|
fields(b, ['kind', 'token'], [], path);
|
|
178
200
|
else if (k === 'motion-clip' || k === 'motion-speed' || k === 'motion-paused')
|
|
179
201
|
fields(b, ['kind', 'group'], [], path);
|
|
180
|
-
else if (k === 'status')
|
|
181
|
-
fields(b, ['kind', 'port'], [], path);
|
|
182
202
|
else if (k === 'pose')
|
|
183
203
|
fields(b, ['kind', 'axis'], ['seconds'], path);
|
|
184
204
|
else if (k === 'items')
|
|
185
205
|
fields(b, ['kind'], ['slot', 'id', 'type', 'place', 'stack', 'status'], path);
|
|
186
206
|
else
|
|
187
|
-
fail('ASSET_SCHEMA', `${path}.kind`, `${String(k)} is not a behavior; state, palette, motion-clip, motion-speed, motion-paused,
|
|
207
|
+
fail('ASSET_SCHEMA', `${path}.kind`, `${String(k)} is not a behavior; state, palette, motion-clip, motion-speed, motion-paused, pose or items`);
|
|
188
208
|
if (k === 'state' && b.sourceMode !== 'observed' && b.sourceMode !== 'commanded')
|
|
189
209
|
fail('ASSET_SCHEMA', `${path}.sourceMode`, 'observed or commanded');
|
|
190
210
|
if (k === 'pose' && !['x', 'y', 'z', 'heading'].includes(b.axis))
|
|
191
211
|
fail('ASSET_SCHEMA', `${path}.axis`, 'x, y, z or heading');
|
|
192
212
|
if (k === 'pose' && b.seconds !== undefined && !(finite(b.seconds) && b.seconds >= 0))
|
|
193
213
|
fail('ASSET_SCHEMA', `${path}.seconds`, 'seconds from 0');
|
|
194
|
-
for (const f of ['input', 'token', 'group', '
|
|
214
|
+
for (const f of ['input', 'token', 'group', 'slot', 'id', 'type', 'place', 'stack', 'status'])
|
|
195
215
|
if (Object.hasOwn(b, f) && (typeof b[f] !== 'string' || !b[f]))
|
|
196
216
|
fail('ASSET_SCHEMA', `${path}.${f}`, 'a nonempty string');
|
|
197
217
|
return b;
|
|
@@ -204,8 +224,8 @@ function closedDomain(node) {
|
|
|
204
224
|
return node.values;
|
|
205
225
|
return null;
|
|
206
226
|
}
|
|
207
|
-
/** One interval of a
|
|
208
|
-
function
|
|
227
|
+
/** One interval of a range table, as the board's evaluator reads its key: `from~to`, either end left open. */
|
|
228
|
+
export function figureIntervalOf(key) {
|
|
209
229
|
const parts = key.split('~');
|
|
210
230
|
if (parts.length !== 2)
|
|
211
231
|
return null;
|
|
@@ -216,47 +236,14 @@ function intervalOf(key) {
|
|
|
216
236
|
return null;
|
|
217
237
|
return [from, to];
|
|
218
238
|
}
|
|
239
|
+
const domainOf = (id, path) => (why, message, extra = {}) => refuse(path, message, { reason: 'data-rule-domain', binding: id, why, ...extra });
|
|
219
240
|
/**
|
|
220
|
-
* Whether a binding's results are what its behavior takes, over the whole of what the value can be (the detail
|
|
221
|
-
* check, kept): a number's range, every closed value, both booleans -- never a sample.
|
|
241
|
+
* Whether a value binding's results are what its behavior takes, over the whole of what the value can be (the detail
|
|
242
|
+
* design's check, kept): a number's range, every closed value, both booleans -- never a sample.
|
|
222
243
|
*/
|
|
223
244
|
function checkRule(b, node, targets, path) {
|
|
224
|
-
const domain = (
|
|
245
|
+
const domain = domainOf(b.id, path);
|
|
225
246
|
const behavior = b.behavior;
|
|
226
|
-
/* Is the result one the behavior takes? */
|
|
227
|
-
const output = (value) => {
|
|
228
|
-
if (behavior.kind === 'state') {
|
|
229
|
-
const input = targets.states.get(behavior.input);
|
|
230
|
-
if (!finite(value))
|
|
231
|
-
return domain('output', `a state input takes a number in ${input.unit}`, { value });
|
|
232
|
-
if (value < input.min || value > input.max)
|
|
233
|
-
domain('output', `${value} is outside ${behavior.input}, ${input.min} to ${input.max}`, { value, within: [input.min, input.max] });
|
|
234
|
-
}
|
|
235
|
-
else if (behavior.kind === 'palette') {
|
|
236
|
-
if (!isFigureColour(value))
|
|
237
|
-
domain('output', 'a colour is #RRGGBB', { value });
|
|
238
|
-
}
|
|
239
|
-
else if (behavior.kind === 'motion-clip') {
|
|
240
|
-
if (typeof value !== 'string' || !targets.groups.get(behavior.group).includes(value))
|
|
241
|
-
domain('output', `${String(value)} is not a clip of ${behavior.group}`, { value });
|
|
242
|
-
}
|
|
243
|
-
else if (behavior.kind === 'motion-speed') {
|
|
244
|
-
if (!finite(value) || value < 0 || value > FIGURE_MOTION_SPEED_MAX)
|
|
245
|
-
domain('output', `a motion speed is 0 to ${FIGURE_MOTION_SPEED_MAX}`, { value });
|
|
246
|
-
}
|
|
247
|
-
else if (behavior.kind === 'motion-paused') {
|
|
248
|
-
if (typeof value !== 'boolean')
|
|
249
|
-
domain('output', 'a pause is true or false', { value });
|
|
250
|
-
}
|
|
251
|
-
else if (behavior.kind === 'status') {
|
|
252
|
-
if (typeof value !== 'string' && !finite(value))
|
|
253
|
-
domain('output', 'a status is a code', { value });
|
|
254
|
-
}
|
|
255
|
-
else if (behavior.kind === 'pose') {
|
|
256
|
-
if (!finite(value))
|
|
257
|
-
domain('output', 'a position is a number', { value });
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
247
|
const param = b.param;
|
|
261
248
|
if (behavior.kind === 'items') {
|
|
262
249
|
if (b.rule !== 'value' || param !== undefined)
|
|
@@ -265,44 +252,8 @@ function checkRule(b, node, targets, path) {
|
|
|
265
252
|
}
|
|
266
253
|
if (!isValue(node))
|
|
267
254
|
return domain('kind', `${b.accessor} is ${node.kind === 'list' ? 'a list' : 'an object'}; this behavior reads one value`);
|
|
268
|
-
if (b.rule === 'map') {
|
|
269
|
-
if (!param || typeof param !== 'object')
|
|
270
|
-
return domain('cases', 'a map gives a result for each value: { value: result, default }');
|
|
271
|
-
const closed = closedDomain(node);
|
|
272
|
-
const keys = Object.keys(param).filter(k => k !== 'default');
|
|
273
|
-
if (closed)
|
|
274
|
-
for (const k of keys)
|
|
275
|
-
if (!closed.some(v => String(v) === k))
|
|
276
|
-
domain('cases', `${k} is not a value of ${b.accessor}`, { value: k });
|
|
277
|
-
const covered = closed ? closed.every(v => Object.hasOwn(param, String(v))) : false;
|
|
278
|
-
if (!covered && !Object.hasOwn(param, 'default'))
|
|
279
|
-
domain('default', closed ? `not every value of ${b.accessor} has a result; give a default` : `${b.accessor} is open; give a default`);
|
|
280
|
-
for (const k of Object.keys(param))
|
|
281
|
-
output(param[k]);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (b.rule === 'range') {
|
|
285
|
-
if (node.kind !== 'number')
|
|
286
|
-
return domain('kind', `a range reads a number; ${b.accessor} is ${node.kind}`);
|
|
287
|
-
if (!param || typeof param !== 'object')
|
|
288
|
-
return domain('cases', "a range gives a result for each interval: { 'from~to': result, default }");
|
|
289
|
-
const intervals = [];
|
|
290
|
-
for (const k of Object.keys(param)) {
|
|
291
|
-
if (k === 'default')
|
|
292
|
-
continue;
|
|
293
|
-
const iv = intervalOf(k);
|
|
294
|
-
if (!iv)
|
|
295
|
-
domain('cases', `${k} is not an interval: from~to, either end may be left out`, { value: k });
|
|
296
|
-
intervals.push(iv);
|
|
297
|
-
}
|
|
298
|
-
if (!Object.hasOwn(param, 'default') && !covers(intervals, node.min ?? -Infinity, node.max ?? Infinity))
|
|
299
|
-
domain('default', `the intervals leave values of ${b.accessor} without a result; give a default`);
|
|
300
|
-
for (const k of Object.keys(param))
|
|
301
|
-
output(param[k]);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
255
|
if (b.rule !== 'value' && b.rule !== 'scale')
|
|
305
|
-
return domain('kind', `${String(b.rule)} is not a rule; value,
|
|
256
|
+
return domain('kind', `${String(b.rule)} is not a rule; value, scale or table`);
|
|
306
257
|
/* value and scale: the value itself, or scale × value + offset. */
|
|
307
258
|
let scale = 1;
|
|
308
259
|
let offset = 0;
|
|
@@ -316,28 +267,25 @@ function checkRule(b, node, targets, path) {
|
|
|
316
267
|
}
|
|
317
268
|
else if (param !== undefined)
|
|
318
269
|
return domain('cases', 'value takes no param');
|
|
319
|
-
if (behavior.kind === 'status') {
|
|
320
|
-
if (b.rule === 'scale')
|
|
321
|
-
domain('kind', 'a status is a code; it is read as it is or by a map');
|
|
322
|
-
if (node.kind === 'boolean')
|
|
323
|
-
domain('kind', 'a status is a code: a number or a text');
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
270
|
if (behavior.kind === 'motion-clip' || behavior.kind === 'palette') {
|
|
327
271
|
if (b.rule === 'scale' || node.kind !== 'text' || !node.values)
|
|
328
|
-
return domain('kind', `${behavior.kind === 'palette' ? 'a colour' : 'a clip'} is read as it is only from a text of fixed values; otherwise by a
|
|
329
|
-
for (const v of node.values)
|
|
330
|
-
|
|
272
|
+
return domain('kind', `${behavior.kind === 'palette' ? 'a colour' : 'a clip'} is read as it is only from a text of fixed values; otherwise by a table`);
|
|
273
|
+
for (const v of node.values) {
|
|
274
|
+
if (behavior.kind === 'palette' && !isFigureColour(v))
|
|
275
|
+
domain('output', 'a colour is #RRGGBB', { value: v });
|
|
276
|
+
if (behavior.kind === 'motion-clip' && !targets.groups.get(behavior.group).includes(v))
|
|
277
|
+
domain('output', `${v} is not a clip of ${behavior.group}`, { value: v });
|
|
278
|
+
}
|
|
331
279
|
return;
|
|
332
280
|
}
|
|
333
281
|
if (behavior.kind === 'motion-paused') {
|
|
334
282
|
if (b.rule === 'scale' || node.kind !== 'boolean')
|
|
335
|
-
domain('kind', 'a pause is read as it is only from true or false; otherwise by a
|
|
283
|
+
domain('kind', 'a pause is read as it is only from true or false; otherwise by a table');
|
|
336
284
|
return;
|
|
337
285
|
}
|
|
338
286
|
/* state, motion-speed and pose read a number. */
|
|
339
287
|
if (node.kind !== 'number')
|
|
340
|
-
return domain('kind', `${b.accessor} is ${node.kind === 'text' ? 'a text' : 'true or false'}; this behavior reads a number, or a value by a
|
|
288
|
+
return domain('kind', `${b.accessor} is ${node.kind === 'text' ? 'a text' : 'true or false'}; this behavior reads a number, or a value by a table`);
|
|
341
289
|
if (behavior.kind === 'pose') {
|
|
342
290
|
const unit = behavior.axis === 'heading' ? 'deg' : 'mm';
|
|
343
291
|
if (b.rule === 'value' && node.unit !== unit)
|
|
@@ -374,10 +322,165 @@ function covers(intervals, min, max) {
|
|
|
374
322
|
}
|
|
375
323
|
return false;
|
|
376
324
|
}
|
|
325
|
+
/** A legend role a consumer chooses a legend by: lower case letters, digits and `-`. */
|
|
326
|
+
const LEGEND_ROLE = /^[a-z0-9][a-z0-9-]*$/;
|
|
327
|
+
/**
|
|
328
|
+
* A table binding (ADR-0166 3, 6): its targets are the figure's, its lookup fits the field, every look is one its
|
|
329
|
+
* targets take and has no column for another, a closed field's every value has a look or there is a default, and a range
|
|
330
|
+
* table's intervals do not overlap.
|
|
331
|
+
*/
|
|
332
|
+
function checkTable(b, node, targets, path) {
|
|
333
|
+
const domain = domainOf(b.id, path);
|
|
334
|
+
const t = b.targets;
|
|
335
|
+
if (!t || typeof t !== 'object' || Array.isArray(t))
|
|
336
|
+
fail('ASSET_SCHEMA', `${path}.targets`, 'object required');
|
|
337
|
+
fields(t, [], ['parts', 'inputs', 'group'], `${path}.targets`);
|
|
338
|
+
for (const k of ['parts', 'inputs'])
|
|
339
|
+
if (t[k] !== undefined && (!Array.isArray(t[k]) || t[k].some((x) => typeof x !== 'string' || !x)))
|
|
340
|
+
fail('ASSET_SCHEMA', `${path}.targets.${k}`, 'a list of ids');
|
|
341
|
+
if (t.group !== undefined && (typeof t.group !== 'string' || !t.group))
|
|
342
|
+
fail('ASSET_SCHEMA', `${path}.targets.group`, 'a nonempty string');
|
|
343
|
+
const target = (what, message, id) => refuse(`${path}.targets`, message, { reason: 'data-table-target', binding: b.id, what, ...(id !== undefined ? { id } : {}) });
|
|
344
|
+
const parts = t.parts ?? [];
|
|
345
|
+
const inputs = t.inputs ?? [];
|
|
346
|
+
if (!parts.length && !inputs.length && t.group === undefined)
|
|
347
|
+
target('none', 'a table shows on a part, a state input, a motion group, or more');
|
|
348
|
+
parts.forEach((p, i) => {
|
|
349
|
+
if (!targets.parts.has(p))
|
|
350
|
+
target('part', `${p} is not a part of this figure`, p);
|
|
351
|
+
if (parts.indexOf(p) !== i)
|
|
352
|
+
target('part', `${p} is named twice`, p);
|
|
353
|
+
});
|
|
354
|
+
inputs.forEach((s, i) => {
|
|
355
|
+
if (!targets.states.has(s))
|
|
356
|
+
target('input', `${s} is not a state input of this figure`, s);
|
|
357
|
+
if (inputs.indexOf(s) !== i)
|
|
358
|
+
target('input', `${s} is named twice`, s);
|
|
359
|
+
});
|
|
360
|
+
if (t.group !== undefined && !targets.groups.has(t.group))
|
|
361
|
+
target('group', `${t.group} is not a motion group of this figure`, t.group);
|
|
362
|
+
if (b.lookup !== 'code' && b.lookup !== 'ranges')
|
|
363
|
+
fail('ASSET_SCHEMA', `${path}.lookup`, 'code or ranges');
|
|
364
|
+
if (!isValue(node))
|
|
365
|
+
return domain('kind', `${b.accessor} is ${node.kind === 'list' ? 'a list' : 'an object'}; a table reads one value`);
|
|
366
|
+
if (b.lookup === 'code' && node.kind === 'number')
|
|
367
|
+
return domain('kind', `${b.accessor} is a number; a table reads it by ranges`);
|
|
368
|
+
if (b.lookup === 'ranges' && node.kind !== 'number')
|
|
369
|
+
return domain('kind', `a range table reads a number; ${b.accessor} is ${node.kind === 'text' ? 'a text' : 'true or false'}`);
|
|
370
|
+
const table = b.table;
|
|
371
|
+
if (!table || typeof table !== 'object' || Array.isArray(table))
|
|
372
|
+
fail('ASSET_SCHEMA', `${path}.table`, 'an object of looks');
|
|
373
|
+
const keys = Object.keys(table).filter(k => k !== 'default');
|
|
374
|
+
if (b.lookup === 'code') {
|
|
375
|
+
const closed = closedDomain(node);
|
|
376
|
+
if (closed) {
|
|
377
|
+
for (const k of keys)
|
|
378
|
+
if (!closed.some(v => String(v) === k))
|
|
379
|
+
domain('cases', `${k} is not a value of ${b.accessor}`, { value: k });
|
|
380
|
+
if (!closed.every(v => Object.hasOwn(table, String(v))) && !Object.hasOwn(table, 'default'))
|
|
381
|
+
domain('default', `not every value of ${b.accessor} has a look; give a default`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const intervals = [];
|
|
386
|
+
for (const k of keys) {
|
|
387
|
+
const iv = figureIntervalOf(k);
|
|
388
|
+
if (!iv)
|
|
389
|
+
return domain('cases', `${k} is not an interval: from~to, either end may be left out`, { value: k });
|
|
390
|
+
const other = intervals.find(([from, to]) => iv[0] < to && from < iv[1]);
|
|
391
|
+
if (other)
|
|
392
|
+
domain('overlap', `${other[2]} and ${k} overlap; a value has one look`, { value: k });
|
|
393
|
+
intervals.push([iv[0], iv[1], k]);
|
|
394
|
+
}
|
|
395
|
+
const n = node;
|
|
396
|
+
if (!Object.hasOwn(table, 'default') && !covers(intervals.map(([from, to]) => [from, to]), n.min ?? -Infinity, n.max ?? Infinity))
|
|
397
|
+
domain('default', `the intervals leave values of ${b.accessor} without a look; give a default`);
|
|
398
|
+
}
|
|
399
|
+
for (const [key, look] of Object.entries(table)) {
|
|
400
|
+
const at = `${path}.table.${key}`;
|
|
401
|
+
if (!look || typeof look !== 'object' || Array.isArray(look))
|
|
402
|
+
fail('ASSET_SCHEMA', at, 'object required');
|
|
403
|
+
fields(look, [], ['color', 'effect', 'values', 'clip', 'speed', 'paused'], at);
|
|
404
|
+
const bad = (what, message, extra = {}) => refuse(`${at}.${what === 'column' ? extra.id : what}`, message, { reason: 'data-table-entry', binding: b.id, key, what, ...extra });
|
|
405
|
+
const column = (name, has) => {
|
|
406
|
+
if (look[name] !== undefined && !has)
|
|
407
|
+
bad('column', `${name} goes to no target of ${b.id}`, { id: name });
|
|
408
|
+
};
|
|
409
|
+
column('color', parts.length > 0);
|
|
410
|
+
column('effect', parts.length > 0);
|
|
411
|
+
column('values', inputs.length > 0);
|
|
412
|
+
for (const [name] of GROUP_COLUMNS)
|
|
413
|
+
column(name, t.group !== undefined);
|
|
414
|
+
if (look.color !== undefined && !isFigureColour(look.color))
|
|
415
|
+
bad('color', 'a colour is #RRGGBB', { value: look.color });
|
|
416
|
+
if (look.effect !== undefined) {
|
|
417
|
+
const e = look.effect;
|
|
418
|
+
if (!e || typeof e !== 'object' || Array.isArray(e))
|
|
419
|
+
fail('ASSET_SCHEMA', `${at}.effect`, 'object required');
|
|
420
|
+
fields(e, ['kind'], ['hz'], `${at}.effect`);
|
|
421
|
+
if (e.kind !== 'blink' && e.kind !== 'glow')
|
|
422
|
+
bad('effect', 'an effect is blink or glow', { value: e.kind });
|
|
423
|
+
if (e.hz !== undefined && !(finite(e.hz) && e.hz > 0))
|
|
424
|
+
bad('effect', 'hz is a positive number', { value: e.hz });
|
|
425
|
+
}
|
|
426
|
+
if (look.values !== undefined) {
|
|
427
|
+
if (!look.values || typeof look.values !== 'object' || Array.isArray(look.values))
|
|
428
|
+
fail('ASSET_SCHEMA', `${at}.values`, 'an object of values');
|
|
429
|
+
for (const [id, v] of Object.entries(look.values)) {
|
|
430
|
+
if (!inputs.includes(id))
|
|
431
|
+
bad('values', `${id} is not a target of ${b.id}`, { id });
|
|
432
|
+
const input = targets.states.get(id);
|
|
433
|
+
if (!finite(v) || v < input.min || v > input.max)
|
|
434
|
+
bad('values', `${String(v)} is outside ${id}, ${input.min} to ${input.max}`, { id, value: v });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (look.clip !== undefined && (typeof look.clip !== 'string' || !targets.groups.get(t.group).includes(look.clip)))
|
|
438
|
+
bad('clip', `${String(look.clip)} is not a clip of ${t.group}`, { value: look.clip });
|
|
439
|
+
if (look.speed !== undefined && !(finite(look.speed) && look.speed >= 0 && look.speed <= FIGURE_MOTION_SPEED_MAX))
|
|
440
|
+
bad('speed', `a motion speed is 0 to ${FIGURE_MOTION_SPEED_MAX}`, { value: look.speed });
|
|
441
|
+
if (look.paused !== undefined && typeof look.paused !== 'boolean')
|
|
442
|
+
bad('paused', 'a pause is true or false', { value: look.paused });
|
|
443
|
+
}
|
|
444
|
+
if (b.legend !== undefined) {
|
|
445
|
+
if (!b.legend || typeof b.legend !== 'object' || Array.isArray(b.legend))
|
|
446
|
+
fail('ASSET_SCHEMA', `${path}.legend`, 'object required');
|
|
447
|
+
fields(b.legend, ['role'], [], `${path}.legend`);
|
|
448
|
+
if (typeof b.legend.role !== 'string' || !LEGEND_ROLE.test(b.legend.role))
|
|
449
|
+
refuse(`${path}.legend.role`, `${String(b.legend.role)} is not a role: lower case letters, digits and -`, { reason: 'data-legend-invalid', binding: b.id, role: String(b.legend.role) });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
377
452
|
function checkBinding(b, schema, targets, path) {
|
|
378
|
-
|
|
453
|
+
if (!b || typeof b !== 'object' || Array.isArray(b))
|
|
454
|
+
fail('ASSET_SCHEMA', path, 'object required');
|
|
455
|
+
const table = b.rule === 'table';
|
|
456
|
+
if (table)
|
|
457
|
+
fields(b, ['id', 'accessor', 'rule', 'lookup', 'targets', 'table'], ['label', 'legend'], path);
|
|
458
|
+
else
|
|
459
|
+
fields(b, ['id', 'accessor', 'behavior', 'rule'], ['label', 'param'], path);
|
|
379
460
|
if (typeof b.id !== 'string' || !FIGURE_PART_ID.test(b.id))
|
|
380
461
|
refuse(`${path}.id`, `${String(b.id)} is not an id: a letter, then letters, digits, - and _`, { reason: 'data-id-invalid', list: 'bindings', id: String(b.id) });
|
|
462
|
+
if (b.label !== undefined && (typeof b.label !== 'string' || !b.label.trim()))
|
|
463
|
+
fail('ASSET_SCHEMA', `${path}.label`, 'a name shown to a person, or none');
|
|
464
|
+
/* The accessor goes down the schema to what the binding reads. */
|
|
465
|
+
const accessorRefused = (accessor, why) => refuse(`${path}.accessor`, `${accessor}: ${why}`, { reason: 'data-accessor-invalid', binding: b.id, accessor, why });
|
|
466
|
+
const found = () => {
|
|
467
|
+
const steps = parseFigureAccessor(b.accessor);
|
|
468
|
+
if (!steps)
|
|
469
|
+
accessorRefused(String(b.accessor), 'syntax');
|
|
470
|
+
const at = nodeAt(schema, steps);
|
|
471
|
+
if ('why' in at)
|
|
472
|
+
return accessorRefused(b.accessor, at.why);
|
|
473
|
+
return at;
|
|
474
|
+
};
|
|
475
|
+
if (table) {
|
|
476
|
+
const { node } = found();
|
|
477
|
+
if (node.kind === 'list')
|
|
478
|
+
accessorRefused(b.accessor, 'list-needs-index');
|
|
479
|
+
else if (!isValue(node))
|
|
480
|
+
accessorRefused(b.accessor, 'not-a-value');
|
|
481
|
+
checkTable(b, node, targets, path);
|
|
482
|
+
return b;
|
|
483
|
+
}
|
|
381
484
|
const behavior = checkBehavior(b.behavior, `${path}.behavior`);
|
|
382
485
|
if (b.param !== undefined && (!b.param || typeof b.param !== 'object' || Array.isArray(b.param)))
|
|
383
486
|
fail('ASSET_SCHEMA', `${path}.param`, 'an object of results');
|
|
@@ -389,19 +492,10 @@ function checkBinding(b, schema, targets, path) {
|
|
|
389
492
|
absent(behavior.token);
|
|
390
493
|
if ((behavior.kind === 'motion-clip' || behavior.kind === 'motion-speed' || behavior.kind === 'motion-paused') && !targets.groups.has(behavior.group))
|
|
391
494
|
absent(behavior.group);
|
|
392
|
-
|
|
393
|
-
absent(behavior.port);
|
|
394
|
-
/* The accessor goes down the schema to what the behavior reads. */
|
|
395
|
-
const accessorRefused = (accessor, why) => refuse(`${path}.accessor`, `${accessor}: ${why}`, { reason: 'data-accessor-invalid', binding: b.id, accessor, why });
|
|
396
|
-
const steps = parseFigureAccessor(b.accessor);
|
|
397
|
-
if (!steps)
|
|
398
|
-
accessorRefused(String(b.accessor), 'syntax');
|
|
399
|
-
const found = nodeAt(schema, steps);
|
|
400
|
-
if ('why' in found)
|
|
401
|
-
return accessorRefused(b.accessor, found.why);
|
|
495
|
+
const { node } = found();
|
|
402
496
|
if (behavior.kind === 'items') {
|
|
403
|
-
if (
|
|
404
|
-
const item =
|
|
497
|
+
if (node.kind === 'list') {
|
|
498
|
+
const item = node.item;
|
|
405
499
|
if (!behavior.id)
|
|
406
500
|
refuse(`${path}.behavior.id`, 'items from a list say which field of an element is its id', { reason: 'data-accessor-invalid', binding: b.id, accessor: b.accessor, why: 'item-id-required' });
|
|
407
501
|
for (const f of ['id', 'type', 'place', 'stack', 'status']) {
|
|
@@ -418,58 +512,20 @@ function checkBinding(b, schema, targets, path) {
|
|
|
418
512
|
accessorRefused(`${b.accessor}[].${rel}`, 'not-a-value');
|
|
419
513
|
}
|
|
420
514
|
}
|
|
421
|
-
else if (isValue(
|
|
515
|
+
else if (isValue(node)) {
|
|
422
516
|
if (behavior.id || behavior.type || behavior.place || behavior.stack || behavior.status)
|
|
423
517
|
refuse(`${path}.behavior`, 'one value is the id itself; element fields are for a list', { reason: 'data-accessor-invalid', binding: b.id, accessor: b.accessor, why: 'not-a-list' });
|
|
424
518
|
}
|
|
425
519
|
else
|
|
426
520
|
accessorRefused(b.accessor, 'not-a-value');
|
|
427
521
|
}
|
|
428
|
-
else if (
|
|
522
|
+
else if (node.kind === 'list')
|
|
429
523
|
accessorRefused(b.accessor, 'list-needs-index');
|
|
430
|
-
else if (!isValue(
|
|
524
|
+
else if (!isValue(node))
|
|
431
525
|
accessorRefused(b.accessor, 'not-a-value');
|
|
432
|
-
checkRule(b,
|
|
526
|
+
checkRule(b, node, targets, path);
|
|
433
527
|
return b;
|
|
434
528
|
}
|
|
435
|
-
function checkPort(p, targets, path) {
|
|
436
|
-
fields(p, ['id', 'label', 'tokens', 'parts'], ['group', 'states'], path);
|
|
437
|
-
if (typeof p.id !== 'string' || !FIGURE_PART_ID.test(p.id))
|
|
438
|
-
refuse(`${path}.id`, `${String(p.id)} is not an id: a letter, then letters, digits, - and _`, { reason: 'data-id-invalid', list: 'ports', id: String(p.id) });
|
|
439
|
-
const invalid = (what, message, id) => refuse(`${path}.${what}`, message, { reason: 'data-port-invalid', port: p.id, what, ...(id !== undefined ? { id } : {}) });
|
|
440
|
-
if (typeof p.label !== 'string' || !p.label.trim())
|
|
441
|
-
invalid('label', 'a name shown to a person');
|
|
442
|
-
if (!Array.isArray(p.tokens) || !Array.isArray(p.parts))
|
|
443
|
-
fail('ASSET_SCHEMA', path, 'tokens and parts are lists');
|
|
444
|
-
if (p.states !== undefined && !Array.isArray(p.states))
|
|
445
|
-
fail('ASSET_SCHEMA', `${path}.states`, 'a list of state inputs');
|
|
446
|
-
const states = p.states ?? [];
|
|
447
|
-
if (!p.tokens.length && !p.parts.length && !states.length)
|
|
448
|
-
invalid('token', 'a status port shows on a colour token, a part, a state input, or more');
|
|
449
|
-
for (const t of p.tokens)
|
|
450
|
-
if (typeof t !== 'string' || !Object.hasOwn(targets.palette, t))
|
|
451
|
-
invalid('token', `${String(t)} is not a colour of this figure`, String(t));
|
|
452
|
-
for (const part of p.parts)
|
|
453
|
-
if (typeof part !== 'string' || !targets.parts.has(part))
|
|
454
|
-
invalid('part', `${String(part)} is not a part of this figure`, String(part));
|
|
455
|
-
if (p.group !== undefined && (typeof p.group !== 'string' || !targets.groups.has(p.group)))
|
|
456
|
-
invalid('group', `${String(p.group)} is not a motion group of this figure`, String(p.group));
|
|
457
|
-
const offered = new Set();
|
|
458
|
-
states.forEach((s, i) => {
|
|
459
|
-
const at = `${path}.states[${i}]`;
|
|
460
|
-
if (!s || typeof s !== 'object' || Array.isArray(s))
|
|
461
|
-
fail('ASSET_SCHEMA', at, 'object required');
|
|
462
|
-
fields(s, ['input', 'sourceMode'], [], at);
|
|
463
|
-
if (s.sourceMode !== 'observed' && s.sourceMode !== 'commanded')
|
|
464
|
-
fail('ASSET_SCHEMA', `${at}.sourceMode`, 'observed or commanded');
|
|
465
|
-
if (typeof s.input !== 'string' || !targets.states.has(s.input))
|
|
466
|
-
invalid('state', `${String(s.input)} is not a state input of this figure`, String(s.input));
|
|
467
|
-
if (offered.has(s.input))
|
|
468
|
-
invalid('state', `${s.input} is offered twice`, s.input);
|
|
469
|
-
offered.add(s.input);
|
|
470
|
-
});
|
|
471
|
-
return p;
|
|
472
|
-
}
|
|
473
529
|
/**
|
|
474
530
|
* Refuses a data interface the contract does not allow (`DATA_INTERFACE`, or `ASSET_SCHEMA` for its shape). Called by
|
|
475
531
|
* `compileFigureAsset`, after the drivers and motion groups, whose clips a binding may name.
|
|
@@ -478,65 +534,38 @@ export function validateFigureDataInterface(asset) {
|
|
|
478
534
|
const di = asset.dataInterface;
|
|
479
535
|
if (di === undefined)
|
|
480
536
|
return;
|
|
481
|
-
fields(di, ['version', 'schema', 'bindings'], [
|
|
537
|
+
fields(di, ['version', 'schema', 'bindings'], [], 'dataInterface');
|
|
482
538
|
if (di.version !== 'figure-data-interface-2')
|
|
483
539
|
fail('VERSION', 'dataInterface.version', String(di.version));
|
|
484
540
|
if (!Array.isArray(di.schema))
|
|
485
541
|
fail('ASSET_SCHEMA', 'dataInterface.schema', 'a list of fields');
|
|
486
542
|
if (!Array.isArray(di.bindings))
|
|
487
543
|
fail('ASSET_SCHEMA', 'dataInterface.bindings', 'a list of bindings');
|
|
488
|
-
if (di.ports !== undefined && !Array.isArray(di.ports))
|
|
489
|
-
fail('ASSET_SCHEMA', 'dataInterface.ports', 'a list of status ports');
|
|
490
544
|
checkFields(di.schema, 'dataInterface.schema');
|
|
491
545
|
const targets = targetsOf(asset);
|
|
492
|
-
const portIds = new Set();
|
|
493
|
-
/* The state inputs ports offer and the motion groups they select clips in: no other port and no binding writes them (ADR-0051 2.3). */
|
|
494
|
-
const portWrites = new Map();
|
|
495
|
-
const writeOnce = (target, id, writer, path) => {
|
|
496
|
-
const other = portWrites.get(`${target} ${id}`);
|
|
497
|
-
if (other)
|
|
498
|
-
refuse(path, `${other.kind} ${other.id} and ${writer.kind} ${writer.id} both write ${target} ${id}; one data writer each`, {
|
|
499
|
-
reason: 'data-writer-duplicate',
|
|
500
|
-
target,
|
|
501
|
-
id,
|
|
502
|
-
writers: [other, writer]
|
|
503
|
-
});
|
|
504
|
-
portWrites.set(`${target} ${id}`, writer);
|
|
505
|
-
};
|
|
506
|
-
(di.ports ?? []).forEach((p, i) => {
|
|
507
|
-
const port = checkPort(p, targets, `dataInterface.ports[${i}]`);
|
|
508
|
-
if (portIds.has(port.id))
|
|
509
|
-
refuse(`dataInterface.ports[${i}].id`, `${port.id} is declared twice`, { reason: 'data-id-duplicate', list: 'ports', id: port.id });
|
|
510
|
-
portIds.add(port.id);
|
|
511
|
-
(port.states ?? []).forEach((s, j) => writeOnce('state', s.input, { kind: 'port', id: port.id }, `dataInterface.ports[${i}].states[${j}]`));
|
|
512
|
-
if (port.group !== undefined)
|
|
513
|
-
writeOnce('motion-clip', port.group, { kind: 'port', id: port.id }, `dataInterface.ports[${i}].group`);
|
|
514
|
-
});
|
|
515
546
|
const ids = new Set();
|
|
547
|
+
/* One writer each (ADR-0166 6): a state input, a motion group's clip, speed or pause -- by a value binding or a table. */
|
|
516
548
|
const writers = new Map();
|
|
549
|
+
const write = (key, behavior, id, binding, path) => {
|
|
550
|
+
const other = writers.get(key);
|
|
551
|
+
if (other !== undefined)
|
|
552
|
+
refuse(path, `${other} and ${binding} both write ${key}; one writer each`, { reason: 'data-behavior-duplicate', bindings: [other, binding], behavior, id });
|
|
553
|
+
writers.set(key, binding);
|
|
554
|
+
};
|
|
517
555
|
di.bindings.forEach((b, i) => {
|
|
518
556
|
const path = `dataInterface.bindings[${i}]`;
|
|
519
557
|
const binding = checkBinding(b, di.schema, targets, path);
|
|
520
558
|
if (ids.has(binding.id))
|
|
521
559
|
refuse(`${path}.id`, `${binding.id} is declared twice`, { reason: 'data-id-duplicate', list: 'bindings', id: binding.id });
|
|
522
560
|
ids.add(binding.id);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (target && portWrites.has(`${target[0]} ${target[1]}`))
|
|
527
|
-
writeOnce(target[0], target[1], { kind: 'binding', id: binding.id }, `${path}.behavior`);
|
|
528
|
-
const key = writeKey(binding.behavior);
|
|
529
|
-
if (key === null)
|
|
561
|
+
if (binding.rule === 'table') {
|
|
562
|
+
for (const w of tableWrites(binding))
|
|
563
|
+
write(w.key, w.behavior, w.id, binding.id, `${path}.targets`);
|
|
530
564
|
return;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
bindings: [other, binding.id],
|
|
536
|
-
behavior: binding.behavior.kind,
|
|
537
|
-
id: behaviorId(binding.behavior)
|
|
538
|
-
});
|
|
539
|
-
writers.set(key, binding.id);
|
|
565
|
+
}
|
|
566
|
+
const key = writeKey(binding.behavior);
|
|
567
|
+
if (key !== null)
|
|
568
|
+
write(key, binding.behavior.kind, behaviorId(binding.behavior), binding.id, `${path}.behavior`);
|
|
540
569
|
});
|
|
541
570
|
}
|
|
542
571
|
/** The data interface of an asset, checked, with what the schema has at each binding; null when the figure declares none. */
|
|
@@ -548,7 +577,7 @@ export function compileFigureDataInterface(asset) {
|
|
|
548
577
|
const bindings = di.bindings.map(binding => {
|
|
549
578
|
const node = nodeAt(di.schema, parseFigureAccessor(binding.accessor)).node;
|
|
550
579
|
let itemNodes;
|
|
551
|
-
if (binding.behavior.kind === 'items' && node.kind === 'list') {
|
|
580
|
+
if (binding.rule !== 'table' && binding.behavior.kind === 'items' && node.kind === 'list') {
|
|
552
581
|
const item = node.item;
|
|
553
582
|
itemNodes = {};
|
|
554
583
|
for (const role of ['id', 'type', 'place', 'stack', 'status']) {
|
|
@@ -576,7 +605,7 @@ export function compileFigureDataInterface(asset) {
|
|
|
576
605
|
}
|
|
577
606
|
};
|
|
578
607
|
walk(di.schema, '');
|
|
579
|
-
return Object.freeze({ schema: Object.freeze(di.schema),
|
|
608
|
+
return Object.freeze({ schema: Object.freeze(di.schema), bindings: Object.freeze(bindings), jsonFields: Object.freeze(jsonFields) });
|
|
580
609
|
}
|
|
581
610
|
/** The schema as lines, parents before their fields, with the bindings that read each place. */
|
|
582
611
|
export function figureDataRows(di) {
|
|
@@ -587,7 +616,7 @@ export function figureDataRows(di) {
|
|
|
587
616
|
const asRow = (accessor) => accessor.replace(/\[\d+\]/g, '[]');
|
|
588
617
|
for (const b of di.bindings) {
|
|
589
618
|
note(asRow(b.accessor), b.id);
|
|
590
|
-
if (b.behavior.kind === 'items')
|
|
619
|
+
if (b.rule !== 'table' && b.behavior.kind === 'items')
|
|
591
620
|
for (const rel of [b.behavior.id, b.behavior.type, b.behavior.place, b.behavior.stack, b.behavior.status])
|
|
592
621
|
if (rel)
|
|
593
622
|
note(`${asRow(b.accessor)}[].${asRow(rel)}`, b.id);
|
|
@@ -679,25 +708,36 @@ export function inferFigureDataSchema(sample) {
|
|
|
679
708
|
return inferFields(sample);
|
|
680
709
|
}
|
|
681
710
|
/* ---------------------------------------------------------------- renames */
|
|
682
|
-
/**
|
|
711
|
+
/**
|
|
712
|
+
* The interface after a part was renamed: a state input the part owned (`<part>.…`) and a table's part follow it -- in
|
|
713
|
+
* a table, its targets and the values of its looks.
|
|
714
|
+
*/
|
|
683
715
|
export function renameDataInterface(asset, from, to) {
|
|
684
716
|
if (!asset.dataInterface)
|
|
685
717
|
return;
|
|
686
718
|
const moved = (id) => (id === from ? to : id.startsWith(`${from}.`) ? to + id.slice(from.length) : id);
|
|
687
719
|
asset.dataInterface = {
|
|
688
720
|
...asset.dataInterface,
|
|
689
|
-
|
|
690
|
-
|
|
721
|
+
bindings: asset.dataInterface.bindings.map(b => {
|
|
722
|
+
if (b.rule !== 'table')
|
|
723
|
+
return b.behavior.kind === 'state' ? { ...b, behavior: { ...b.behavior, input: moved(b.behavior.input) } } : b;
|
|
724
|
+
const { parts, inputs, group } = b.targets;
|
|
725
|
+
return {
|
|
726
|
+
...b,
|
|
727
|
+
targets: { ...(parts ? { parts: parts.map(moved) } : {}), ...(inputs ? { inputs: inputs.map(moved) } : {}), ...(group !== undefined ? { group } : {}) },
|
|
728
|
+
table: Object.fromEntries(Object.entries(b.table).map(([key, look]) => [key, look.values ? { ...look, values: Object.fromEntries(Object.entries(look.values).map(([id, v]) => [moved(id), v])) } : look]))
|
|
729
|
+
};
|
|
730
|
+
})
|
|
691
731
|
};
|
|
692
732
|
}
|
|
693
|
-
/** The interface after a clip was renamed: every
|
|
733
|
+
/** The interface after a clip was renamed: every table that selects it in a motion group selects it by its new name. */
|
|
694
734
|
export function renameDataInterfaceClip(asset, group, from, to) {
|
|
695
735
|
if (!asset.dataInterface)
|
|
696
736
|
return;
|
|
697
737
|
asset.dataInterface = {
|
|
698
738
|
...asset.dataInterface,
|
|
699
|
-
bindings: asset.dataInterface.bindings.map(b => b.
|
|
700
|
-
? { ...b,
|
|
739
|
+
bindings: asset.dataInterface.bindings.map(b => b.rule === 'table' && b.targets.group === group
|
|
740
|
+
? { ...b, table: Object.fromEntries(Object.entries(b.table).map(([key, look]) => [key, look.clip === from ? { ...look, clip: to } : look])) }
|
|
701
741
|
: b)
|
|
702
742
|
};
|
|
703
743
|
}
|
|
@@ -749,7 +789,9 @@ export function renameDataField(di, accessor, to) {
|
|
|
749
789
|
return at === names.length - 1 && names.every((name, i) => prefix[i] === name) ? { field: to } : step;
|
|
750
790
|
}));
|
|
751
791
|
};
|
|
752
|
-
out.bindings = out.bindings.map(b => {
|
|
792
|
+
out.bindings = out.bindings.map((b) => {
|
|
793
|
+
if (b.rule === 'table')
|
|
794
|
+
return { ...b, accessor: through(b.accessor) };
|
|
753
795
|
const next = { ...b, accessor: through(b.accessor) };
|
|
754
796
|
if (b.behavior.kind !== 'items')
|
|
755
797
|
return next;
|