@things-factory/figure-ui 10.1.223 → 10.1.224
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/canvas.ts +2 -2
- package/client/modeller/consumer-capabilities.ts +66 -0
- package/client/modeller/data-interface.ts +230 -80
- package/client/modeller/editor.ts +14 -7
- package/client/modeller/figure-capability-panel.ts +145 -1
- package/client/modeller/figure-data-panel.ts +495 -188
- package/client/modeller/figure-links.ts +130 -12
- package/client/modeller/figure-preview.ts +19 -2
- package/client/modeller/figure-slot-layout-panel.ts +7 -3
- package/client/modeller/links.ts +58 -0
- package/client/modeller/refusals.ts +12 -4
- package/client/modeller/slot-layout-host.ts +76 -5
- package/dist-client/modeller/canvas.js +2 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/consumer-capabilities.d.ts +29 -0
- package/dist-client/modeller/consumer-capabilities.js +60 -0
- package/dist-client/modeller/consumer-capabilities.js.map +1 -0
- package/dist-client/modeller/data-interface.d.ts +83 -17
- package/dist-client/modeller/data-interface.js +194 -82
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +5 -2
- package/dist-client/modeller/editor.js +16 -5
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +12 -0
- package/dist-client/modeller/figure-capability-panel.js +123 -1
- package/dist-client/modeller/figure-capability-panel.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +57 -13
- package/dist-client/modeller/figure-data-panel.js +461 -180
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +11 -0
- package/dist-client/modeller/figure-links.js +137 -16
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +4 -0
- package/dist-client/modeller/figure-preview.js +19 -1
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +6 -4
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/links.d.ts +22 -0
- package/dist-client/modeller/links.js +48 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/refusals.js +14 -6
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +28 -3
- package/dist-client/modeller/slot-layout-host.js +68 -6
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/browser/data-table-page.js +9 -0
- package/test/browser/links.check.mjs +25 -3
- package/test/consumer-capabilities.test.ts +116 -0
- package/test/data-motion.test.ts +94 -27
- package/test/links.test.ts +15 -0
- package/test/slot-layout-host.test.ts +63 -5
- package/translations/en.json +81 -23
- package/translations/es.json +81 -23
- package/translations/ja.json +81 -23
- package/translations/ko.json +81 -23
- package/translations/ms.json +81 -23
- package/translations/zh.json +81 -23
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The figure's data and what it moves (ADR-0051 §8,
|
|
3
|
-
*
|
|
2
|
+
* The figure's data and what it moves (ADR-0051 §8, ADR-0166): what the data tab offers and shows -- one binding, its
|
|
3
|
+
* method as the field's kind allows (as it is, scaled, a code table, a range table). Every rule is figure-model's -- which accessor holds, which rule's results a behavior takes -- and the reading
|
|
4
4
|
* of data is things-scene's; this only lists what there is to choose, drafts a binding the kernel then checks, and edits
|
|
5
5
|
* the schema as a tree. Kept apart from the element so it is tested without a browser.
|
|
6
6
|
*/
|
|
@@ -10,16 +10,14 @@ export const behaviorKeyOf = (b) => b.kind === 'state'
|
|
|
10
10
|
? `state:${b.input}`
|
|
11
11
|
: b.kind === 'palette'
|
|
12
12
|
? `palette:${b.token}`
|
|
13
|
-
: b.kind === '
|
|
14
|
-
? `
|
|
15
|
-
: b.kind === '
|
|
16
|
-
? `
|
|
17
|
-
: b.kind
|
|
18
|
-
? `items:${b.slot ?? ''}`
|
|
19
|
-
: `${b.kind}:${b.group}`;
|
|
13
|
+
: b.kind === 'pose'
|
|
14
|
+
? `pose:${b.axis}`
|
|
15
|
+
: b.kind === 'items'
|
|
16
|
+
? `items:${b.slot ?? ''}`
|
|
17
|
+
: `${b.kind}:${b.group}`;
|
|
20
18
|
/**
|
|
21
|
-
* Everything
|
|
22
|
-
*
|
|
19
|
+
* Everything a value binding may drive: each control, each colour, each motion group's clip, speed and pause, its
|
|
20
|
+
* position and heading, and -- when it has places -- the items in them.
|
|
23
21
|
*/
|
|
24
22
|
export function behaviorsOf(asset) {
|
|
25
23
|
const out = [];
|
|
@@ -32,12 +30,10 @@ export function behaviorsOf(asset) {
|
|
|
32
30
|
for (const token of Object.keys(asset.palette))
|
|
33
31
|
add({ kind: 'palette', token }, token);
|
|
34
32
|
for (const group of asset.motionGroups ?? []) {
|
|
35
|
-
add({ kind: 'motion-clip', group: group.id }, group.label);
|
|
33
|
+
add({ kind: 'motion-clip', group: group.id }, group.label, { clips: [...group.clips] });
|
|
36
34
|
add({ kind: 'motion-speed', group: group.id }, group.label, { min: 0, max: FIGURE_MOTION_SPEED_MAX });
|
|
37
35
|
add({ kind: 'motion-paused', group: group.id }, group.label);
|
|
38
36
|
}
|
|
39
|
-
for (const port of asset.dataInterface?.ports ?? [])
|
|
40
|
-
add({ kind: 'status', port: port.id }, port.label);
|
|
41
37
|
for (const axis of ['x', 'z', 'y', 'heading'])
|
|
42
38
|
add({ kind: 'pose', axis, seconds: 1 }, axis, { unit: axis === 'heading' ? 'deg' : 'mm' });
|
|
43
39
|
if (asset.document.capabilities.slotLayout || asset.document.capabilities.names?.includes('Holdable'))
|
|
@@ -45,22 +41,41 @@ export function behaviorsOf(asset) {
|
|
|
45
41
|
return out;
|
|
46
42
|
}
|
|
47
43
|
const isValue = (n) => n.kind === 'number' || n.kind === 'boolean' || n.kind === 'text';
|
|
48
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Whether a field of this kind drives the behavior as it is or scaled -- the choices a value binding offers for it. A
|
|
46
|
+
* value that picks a look per code is a table (ADR-0166 2).
|
|
47
|
+
*/
|
|
49
48
|
export function fitsField(node, choice) {
|
|
50
49
|
if (choice.kind === 'items')
|
|
51
50
|
return node.kind === 'list' || node.kind === 'text' || node.kind === 'number';
|
|
52
51
|
if (!isValue(node))
|
|
53
52
|
return false;
|
|
54
|
-
if (choice.kind === 'pose')
|
|
55
|
-
return node.kind === 'number';
|
|
56
|
-
if (choice.kind === 'motion-speed')
|
|
57
|
-
return node.kind === 'number' || node.kind === 'boolean';
|
|
58
53
|
if (choice.kind === 'motion-paused')
|
|
59
|
-
return node.kind === 'boolean'
|
|
60
|
-
if (choice.kind === '
|
|
61
|
-
return node.kind
|
|
62
|
-
|
|
54
|
+
return node.kind === 'boolean';
|
|
55
|
+
if (choice.kind === 'palette')
|
|
56
|
+
return node.kind === 'text' && !!node.values?.every(v => /^#[0-9a-f]{6}$/i.test(v));
|
|
57
|
+
if (choice.kind === 'motion-clip') {
|
|
58
|
+
const clips = choice.behavior.kind === 'motion-clip' ? choice.clips : undefined;
|
|
59
|
+
return node.kind === 'text' && !!node.values && (!clips || node.values.every(v => clips.includes(v)));
|
|
60
|
+
}
|
|
61
|
+
return node.kind === 'number';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The methods a field allows (ADR-0166 2): a number as it is, scaled or by a range table; a text or true/false by a code
|
|
65
|
+
* table, or as it is where a behavior takes it so; a list as it is (its items). A new field is made by a table.
|
|
66
|
+
*/
|
|
67
|
+
export function methodsOf(node, choices) {
|
|
68
|
+
if (!node)
|
|
69
|
+
return ['code', 'ranges'];
|
|
70
|
+
const fitting = choices.some(c => fitsField(node, c));
|
|
71
|
+
if (node.kind === 'number')
|
|
72
|
+
return [...(fitting ? ['value', 'scale'] : []), 'ranges'];
|
|
73
|
+
if (node.kind === 'text' || node.kind === 'boolean')
|
|
74
|
+
return [...(fitting ? ['value'] : []), 'code'];
|
|
75
|
+
return fitting ? ['value'] : [];
|
|
63
76
|
}
|
|
77
|
+
/** The method a binding is shown by. */
|
|
78
|
+
export const methodOf = (b) => (b.rule === 'table' ? (b.lookup === 'code' ? 'code' : 'ranges') : b.rule);
|
|
64
79
|
/** An id not taken among `taken`: `base`, then `base-2`, `base-3`, … made into the kernel's id shape. */
|
|
65
80
|
export function freeIdOf(taken, base) {
|
|
66
81
|
const used = new Set(taken);
|
|
@@ -79,19 +94,15 @@ const TO_DEG = { deg: 1, rad: 180 / Math.PI };
|
|
|
79
94
|
/** The values a closed field takes: a closed text's values, both booleans. Empty when open. */
|
|
80
95
|
export const domainOf = (node) => (node.kind === 'boolean' ? [false, true] : node.kind === 'text' && node.values ? [...node.values] : []);
|
|
81
96
|
/**
|
|
82
|
-
* A first binding of a field to a behavior, for the author to adjust: the value as it is when it already fits, a
|
|
83
|
-
* when only its unit or range differs
|
|
84
|
-
* Each result starts at what the behavior shows now -- the control's rest value, the colour, the group's default clip --
|
|
85
|
-
* or, for a clip, the clip of the value's own name. The kernel says whether it holds.
|
|
97
|
+
* A first value binding of a field to a behavior, for the author to adjust: the value as it is when it already fits, a
|
|
98
|
+
* scale when only its unit or range differs -- or the method the author chose. The kernel says whether it holds.
|
|
86
99
|
*/
|
|
87
|
-
export function bindingDraftOf(asset, accessor, node, choice) {
|
|
100
|
+
export function bindingDraftOf(asset, accessor, node, choice, method) {
|
|
88
101
|
const taken = (asset.dataInterface?.bindings ?? []).map(b => b.id);
|
|
89
102
|
const name = accessor.replace(/\[\d+\]/g, '').split('.').pop() ?? accessor;
|
|
90
103
|
const id = freeIdOf(taken, `${name}-${choice.kind === 'state' ? choice.behavior.input : choice.kind}`);
|
|
91
104
|
const behavior = choice.behavior;
|
|
92
105
|
const make = (rule, param) => ({ id, accessor, behavior, rule, ...(param ? { param } : {}) });
|
|
93
|
-
const domain = domainOf(node);
|
|
94
|
-
const caseMap = (each, fallback) => Object.fromEntries([...domain.map(v => [String(v), each(v)]), ...(domain.length ? [] : [['default', fallback]])]);
|
|
95
106
|
if (behavior.kind === 'items') {
|
|
96
107
|
if (node.kind !== 'list')
|
|
97
108
|
return make('value');
|
|
@@ -103,53 +114,141 @@ export function bindingDraftOf(asset, accessor, node, choice) {
|
|
|
103
114
|
const stack = pick(/stack/i, /level/i);
|
|
104
115
|
return { id, accessor, behavior: { kind: 'items', ...(itemId ? { id: itemId } : {}), ...(type ? { type } : {}), ...(place ? { place } : {}), ...(stack ? { stack } : {}) }, rule: 'value' };
|
|
105
116
|
}
|
|
106
|
-
if (
|
|
117
|
+
if (node.kind !== 'number')
|
|
107
118
|
return make('value');
|
|
108
|
-
if (behavior.kind === 'palette') {
|
|
109
|
-
const now = asset.palette[behavior.token] ?? '#000000';
|
|
110
|
-
if (node.kind === 'number')
|
|
111
|
-
return make('range', { default: now });
|
|
112
|
-
return make('map', caseMap(() => now, now));
|
|
113
|
-
}
|
|
114
|
-
if (behavior.kind === 'motion-clip') {
|
|
115
|
-
const group = (asset.motionGroups ?? []).find(g => g.id === behavior.group);
|
|
116
|
-
const clips = group?.clips ?? [];
|
|
117
|
-
if (node.kind === 'text' && node.values && node.values.every(v => clips.includes(v)))
|
|
118
|
-
return make('value');
|
|
119
|
-
return make('map', caseMap(v => (clips.includes(String(v)) ? String(v) : (group?.defaultClip ?? '')), group?.defaultClip ?? ''));
|
|
120
|
-
}
|
|
121
|
-
if (behavior.kind === 'motion-paused') {
|
|
122
|
-
if (node.kind === 'boolean')
|
|
123
|
-
return make('value');
|
|
124
|
-
return make('map', caseMap(() => false, false));
|
|
125
|
-
}
|
|
126
119
|
if (behavior.kind === 'pose') {
|
|
127
|
-
if (node.kind !== 'number')
|
|
128
|
-
return make('value');
|
|
129
120
|
const want = behavior.axis === 'heading' ? TO_DEG : TO_MM;
|
|
130
121
|
const unit = node.unit ?? '';
|
|
131
|
-
if (unit === (behavior.axis === 'heading' ? 'deg' : 'mm'))
|
|
122
|
+
if (method === 'value' || unit === (behavior.axis === 'heading' ? 'deg' : 'mm') || (method === undefined && !(unit in want)))
|
|
132
123
|
return make('value');
|
|
133
|
-
return
|
|
124
|
+
return make('scale', { scale: want[unit] ?? 1, offset: 0 });
|
|
134
125
|
}
|
|
135
126
|
/* state and motion-speed: a number into a range. */
|
|
136
127
|
const [lo, hi, unit] = behavior.kind === 'motion-speed' ? [0, FIGURE_MOTION_SPEED_MAX, 'ratio'] : [choice.min ?? 0, choice.max ?? 1, choice.unit ?? ''];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (sameUnit && inside)
|
|
141
|
-
return make('value');
|
|
142
|
-
if (node.min !== undefined && node.max !== undefined && node.max > node.min) {
|
|
143
|
-
/* The field's range laid onto the behavior's. */
|
|
144
|
-
const scale = (hi - lo) / (node.max - node.min);
|
|
145
|
-
return make('scale', { scale: round(scale), offset: round(lo - scale * node.min) });
|
|
146
|
-
}
|
|
128
|
+
const sameUnit = behavior.kind === 'motion-speed' ? node.unit === undefined || node.unit === 'ratio' : node.unit === unit;
|
|
129
|
+
const inside = node.min !== undefined && node.max !== undefined && node.min >= lo && node.max <= hi;
|
|
130
|
+
if (method === 'value' || (method === undefined && sameUnit && inside))
|
|
147
131
|
return make('value');
|
|
132
|
+
if (node.min !== undefined && node.max !== undefined && node.max > node.min) {
|
|
133
|
+
/* The field's range laid onto the behavior's. */
|
|
134
|
+
const scale = (hi - lo) / (node.max - node.min);
|
|
135
|
+
return make('scale', { scale: round(scale), offset: round(lo - scale * node.min) });
|
|
148
136
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
return method === 'scale' ? make('scale', { scale: 1, offset: 0 }) : make('value');
|
|
138
|
+
}
|
|
139
|
+
/* ---------------------------------------------------------------- tables (ADR-0166 3) */
|
|
140
|
+
/** The first code a new code table names, and the next ones: `CODE`, `CODE2`, … */
|
|
141
|
+
export function freeCodeOf(taken) {
|
|
142
|
+
if (!taken.includes('CODE'))
|
|
143
|
+
return 'CODE';
|
|
144
|
+
let n = 2;
|
|
145
|
+
while (taken.includes(`CODE${n}`))
|
|
146
|
+
n++;
|
|
147
|
+
return `CODE${n}`;
|
|
148
|
+
}
|
|
149
|
+
/** A legend role from a binding's id: lower case letters, digits and `-`. */
|
|
150
|
+
export const legendRoleOf = (id) => id
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
153
|
+
.replace(/^-+|-+$/g, '') || 'legend';
|
|
154
|
+
/**
|
|
155
|
+
* A first table on a field (ADR-0166 10): a row per value of a closed field, an open field's rows typed later, one
|
|
156
|
+
* interval over the whole of a number. A new field is made by the table (P2): a code table's a text whose values are the
|
|
157
|
+
* table's keys, starting with one code; a range table's a number. Returns the binding and, for a new field, the schema
|
|
158
|
+
* with it.
|
|
159
|
+
*/
|
|
160
|
+
export function tableDraftOf(asset, field, method, targets) {
|
|
161
|
+
const taken = (asset.dataInterface?.bindings ?? []).map(b => b.id);
|
|
162
|
+
const made = 'name' in field;
|
|
163
|
+
const name = made ? field.name : (field.path.replace(/\[\]/g, '').split('.').pop() ?? field.path);
|
|
164
|
+
const id = freeIdOf(taken, name.toLowerCase());
|
|
165
|
+
const node = made ? (method === 'code' ? { kind: 'text', values: ['CODE'] } : { kind: 'number' }) : field.node;
|
|
166
|
+
const accessor = made ? field.name : accessorOfRow(field.path);
|
|
167
|
+
const table = method === 'code' ? Object.fromEntries(domainOf(node).map(v => [String(v), {}])) : { [`${node.kind === 'number' ? (node.min ?? '') : ''}~`]: {} };
|
|
168
|
+
const binding = { id, accessor, rule: 'table', lookup: method, targets: tidyTargets(targets), table };
|
|
169
|
+
if (!made)
|
|
170
|
+
return { binding };
|
|
171
|
+
const schema = [...(asset.dataInterface?.schema ?? []), { name: field.name, ...node }];
|
|
172
|
+
return { binding, schema };
|
|
173
|
+
}
|
|
174
|
+
/** Targets with empty lists left out, as the kernel keeps them. */
|
|
175
|
+
const tidyTargets = (t) => ({
|
|
176
|
+
...(t.parts?.length ? { parts: [...t.parts] } : {}),
|
|
177
|
+
...(t.inputs?.length ? { inputs: [...t.inputs] } : {}),
|
|
178
|
+
...(t.group ? { group: t.group } : {})
|
|
179
|
+
});
|
|
180
|
+
/** A look with only the columns its targets take: a target taken away takes its columns with it. */
|
|
181
|
+
function lookFor(look, t) {
|
|
182
|
+
const out = {};
|
|
183
|
+
if (t.parts?.length) {
|
|
184
|
+
if (look.color !== undefined)
|
|
185
|
+
out.color = look.color;
|
|
186
|
+
if (look.effect !== undefined)
|
|
187
|
+
out.effect = look.effect;
|
|
188
|
+
}
|
|
189
|
+
if (t.inputs?.length && look.values) {
|
|
190
|
+
const values = Object.fromEntries(Object.entries(look.values).filter(([id]) => t.inputs.includes(id)));
|
|
191
|
+
if (Object.keys(values).length)
|
|
192
|
+
out.values = values;
|
|
193
|
+
}
|
|
194
|
+
if (t.group) {
|
|
195
|
+
if (look.clip !== undefined)
|
|
196
|
+
out.clip = look.clip;
|
|
197
|
+
if (look.speed !== undefined)
|
|
198
|
+
out.speed = look.speed;
|
|
199
|
+
if (look.paused !== undefined)
|
|
200
|
+
out.paused = look.paused;
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/** The table with these targets: each look keeps only the columns they take. */
|
|
205
|
+
export function tableWithTargets(b, targets) {
|
|
206
|
+
const t = tidyTargets(targets);
|
|
207
|
+
return { ...b, targets: t, table: Object.fromEntries(Object.entries(b.table).map(([k, look]) => [k, lookFor(look, t)])) };
|
|
208
|
+
}
|
|
209
|
+
/** The table with one look set: the whole look given, a column left out cleared. */
|
|
210
|
+
export const tableWithLook = (b, key, look) => ({ ...b, table: { ...b.table, [key]: lookFor(look, b.targets) } });
|
|
211
|
+
/** The table's rows renamed, added or taken away, in order; `default` kept last. */
|
|
212
|
+
export function tableWithKeys(b, rows) {
|
|
213
|
+
const d = b.table.default;
|
|
214
|
+
return { ...b, table: { ...Object.fromEntries(rows), ...(d !== undefined ? { default: d } : {}) } };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The columns a table's targets give its looks, as the row editor shows them: colour and effect for parts, a value per
|
|
218
|
+
* state input (its unit and range), the clip, speed and pause of the group.
|
|
219
|
+
*/
|
|
220
|
+
export function tableColumnsOf(asset, b) {
|
|
221
|
+
const out = [];
|
|
222
|
+
if (b.targets.parts?.length)
|
|
223
|
+
out.push({ kind: 'color' }, { kind: 'effect' });
|
|
224
|
+
for (const id of b.targets.inputs ?? []) {
|
|
225
|
+
const input = asset.document.model.inputs.find(i => i.id === id);
|
|
226
|
+
out.push({ kind: 'value', id, label: asset.stateInputs?.[id]?.label ?? id, unit: input?.unit, min: input?.min, max: input?.max });
|
|
227
|
+
}
|
|
228
|
+
if (b.targets.group) {
|
|
229
|
+
const group = (asset.motionGroups ?? []).find(g => g.id === b.targets.group);
|
|
230
|
+
out.push({ kind: 'clip', clips: group?.clips ?? [] }, { kind: 'speed' }, { kind: 'paused' });
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/** The state inputs a table may give values to: each control, with its label. */
|
|
235
|
+
export const stateInputsOf = (asset) => asset.document.model.inputs.filter(i => i.role === 'state').map(i => ({ id: i.id, label: asset.stateInputs?.[i.id]?.label ?? i.id }));
|
|
236
|
+
/**
|
|
237
|
+
* A code table's field follows its keys (ADR-0166 risk 3): when the field is a closed text, its values are the table's
|
|
238
|
+
* codes. The schema with the field so, or undefined when the field is open or already so.
|
|
239
|
+
*/
|
|
240
|
+
export function schemaFollowingCodes(asset, b, codes) {
|
|
241
|
+
const schema = asset.dataInterface?.schema ?? [];
|
|
242
|
+
const path = b.accessor.replace(/\[\d+\]/g, '[]');
|
|
243
|
+
const field = fieldAt(schema, path);
|
|
244
|
+
if (field?.kind !== 'text' || !field.values || !codes.length)
|
|
245
|
+
return undefined;
|
|
246
|
+
if (field.values.length === codes.length && field.values.every((v, i) => v === codes[i]))
|
|
247
|
+
return undefined;
|
|
248
|
+
const next = { ...field, values: [...codes] };
|
|
249
|
+
if (next.default !== undefined && !codes.includes(next.default))
|
|
250
|
+
delete next.default;
|
|
251
|
+
return schemaWithFieldReplaced(schema, path, next);
|
|
153
252
|
}
|
|
154
253
|
const round = (v) => Math.round(v * 1e6) / 1e6;
|
|
155
254
|
/* ---------------------------------------------------------------- the schema as a tree */
|
|
@@ -225,12 +324,19 @@ export const schemaRowsOf = (asset) => (asset.dataInterface ? figureDataRows(ass
|
|
|
225
324
|
/* ---------------------------------------------------------------- reading a sample */
|
|
226
325
|
/**
|
|
227
326
|
* What a sample of data would do on this figure, read the one way a placed figure reads it (things-scene's figure
|
|
228
|
-
* inputs): what it moves, or why it is refused, each value's reason
|
|
327
|
+
* inputs): what it moves, or why it is refused, each value's reason; each table's look and where it came from, and what
|
|
328
|
+
* of a legend was left out. `legends`: sample legends by role, as a scene's legend of the role's name would be.
|
|
229
329
|
*/
|
|
230
|
-
export function sampleOf(asset, sample) {
|
|
330
|
+
export function sampleOf(asset, sample, legends = {}) {
|
|
231
331
|
const inputs = new FigureInputs(asset);
|
|
332
|
+
inputs.setThemes({ legends: Object.fromEntries(Object.entries(legends).map(([role, legend]) => [role, { legend, source: 'domain', name: role }])) });
|
|
232
333
|
const taken = inputs.setData(sample);
|
|
233
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
issues: taken.accepted ? [] : (taken.issues[0]?.issues ?? []),
|
|
336
|
+
result: inputs.data,
|
|
337
|
+
uses: inputs.tableUses,
|
|
338
|
+
notes: inputs.diagnostics().filter(i => i.severity === 'note')
|
|
339
|
+
};
|
|
234
340
|
}
|
|
235
341
|
/** The rows a person gives a value by hand, as a board's saved values: one value, read by a binding, not inside a list. */
|
|
236
342
|
export const valueRowsOf = (asset) => schemaRowsOf(asset).filter(r => isValue(r.node) && r.bindings.length > 0 && !r.path.includes('[]'));
|
|
@@ -242,14 +348,8 @@ export function typedValueOf(node, typed) {
|
|
|
242
348
|
return typed === 'true';
|
|
243
349
|
return typed;
|
|
244
350
|
}
|
|
245
|
-
/** The parts a
|
|
351
|
+
/** The parts a table may show on: the parts the figure's appearance names. */
|
|
246
352
|
export const partsOf = (asset) => [...new Set(asset.appearance.map(a => a.target))];
|
|
247
|
-
/** A first status port: the label as given, on the first colour token, no part yet. The kernel says whether it holds. */
|
|
248
|
-
export function portDraftOf(asset, label) {
|
|
249
|
-
const taken = (asset.dataInterface?.ports ?? []).map(p => p.id);
|
|
250
|
-
const token = Object.keys(asset.palette)[0];
|
|
251
|
-
return { id: freeIdOf(taken, label.trim() || 'status'), label: label.trim(), tokens: token ? [token] : [], parts: [] };
|
|
252
|
-
}
|
|
253
353
|
/** Whether a text is an accessor the kernel takes. */
|
|
254
354
|
export const accessorTaken = (accessor) => parseFigureAccessor(accessor) !== null;
|
|
255
355
|
/**
|
|
@@ -259,6 +359,17 @@ export const accessorTaken = (accessor) => parseFigureAccessor(accessor) !== nul
|
|
|
259
359
|
export function motionWrittenByData(asset) {
|
|
260
360
|
const out = {};
|
|
261
361
|
for (const b of asset.dataInterface?.bindings ?? []) {
|
|
362
|
+
if (b.rule === 'table') {
|
|
363
|
+
if (!b.targets.group)
|
|
364
|
+
continue;
|
|
365
|
+
const one = (out[b.targets.group] ??= { clip: false, speed: false });
|
|
366
|
+
const looks = Object.values(b.table);
|
|
367
|
+
if (looks.some(l => l.clip !== undefined))
|
|
368
|
+
one.clip = true;
|
|
369
|
+
if (looks.some(l => l.speed !== undefined))
|
|
370
|
+
one.speed = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
262
373
|
if (b.behavior.kind !== 'motion-clip' && b.behavior.kind !== 'motion-speed')
|
|
263
374
|
continue;
|
|
264
375
|
const one = (out[b.behavior.group] ??= { clip: false, speed: false });
|
|
@@ -287,7 +398,7 @@ export function endlessJointsOf(asset) {
|
|
|
287
398
|
/**
|
|
288
399
|
* The rotation card's commands (data binding design decision 1, 「회전」): a clip that turns the joint once around and
|
|
289
400
|
* keeps adding the turn (wrap, accumulating), a reverse one when a direction is given, the motion group holding them,
|
|
290
|
-
* and the bindings -- on to the group's pause, speed to its speed, direction to its clip. Nothing new runs them: they
|
|
401
|
+
* and the bindings -- on, by a table, to the group's pause, speed to its speed, direction, by a table, to its clip. Nothing new runs them: they
|
|
291
402
|
* are the drivers, the group and the bindings any author could write, and stay editable one by one.
|
|
292
403
|
*
|
|
293
404
|
* The clip lasts `T = 3 × 60 / max rpm` seconds, so the most the field gives plays at the group's top speed, 3 -- the
|
|
@@ -338,7 +449,7 @@ export function rotationStepsOf(asset, c) {
|
|
|
338
449
|
return { kind: 'set-data-binding', binding: { id, ...binding } };
|
|
339
450
|
};
|
|
340
451
|
if (c.on)
|
|
341
|
-
steps.push(bind(`${group}-on`, { accessor: c.on,
|
|
452
|
+
steps.push(bind(`${group}-on`, { accessor: c.on, rule: 'table', lookup: 'code', targets: { group }, table: { true: { paused: false }, false: { paused: true } } }));
|
|
342
453
|
if (c.speed)
|
|
343
454
|
steps.push(bind(`${group}-speed`, { accessor: c.speed.path, behavior: { kind: 'motion-speed', group }, rule: 'scale', param: { scale: round(seconds / 60), offset: 0 } }));
|
|
344
455
|
if (c.direction) {
|
|
@@ -346,9 +457,10 @@ export function rotationStepsOf(asset, c) {
|
|
|
346
457
|
const covered = values.every(v => v === c.direction.forward || v === c.direction.reverse);
|
|
347
458
|
steps.push(bind(`${group}-direction`, {
|
|
348
459
|
accessor: c.direction.path,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
460
|
+
rule: 'table',
|
|
461
|
+
lookup: 'code',
|
|
462
|
+
targets: { group },
|
|
463
|
+
table: { [c.direction.forward]: { clip: forward }, [c.direction.reverse]: { clip: reverse }, ...(covered ? {} : { default: { clip: forward } }) }
|
|
352
464
|
}));
|
|
353
465
|
}
|
|
354
466
|
return steps;
|