stickyflow 0.4.0 → 0.5.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/CHANGELOG.md +27 -0
- package/README.md +51 -5
- package/dist/binding/index.cjs +34 -0
- package/dist/binding/index.cjs.map +1 -0
- package/dist/binding/index.d.cts +11 -0
- package/dist/binding/index.d.ts +11 -0
- package/dist/binding/index.js +5 -0
- package/dist/binding/index.js.map +1 -0
- package/dist/{chunk-XEN5QIIJ.js → chunk-7VW2XKVB.js} +3 -3
- package/dist/{chunk-XEN5QIIJ.js.map → chunk-7VW2XKVB.js.map} +1 -1
- package/dist/chunk-BODZN3ZI.js +1182 -0
- package/dist/chunk-BODZN3ZI.js.map +1 -0
- package/dist/{chunk-CSXQD3ON.js → chunk-G7NRK5AG.js} +1128 -1081
- package/dist/chunk-G7NRK5AG.js.map +1 -0
- package/dist/{chunk-BYMIYXP5.js → chunk-IEHE6WVN.js} +197 -19
- package/dist/chunk-IEHE6WVN.js.map +1 -0
- package/dist/{chunk-HFOGKSNU.cjs → chunk-L3VJXZFD.cjs} +15 -15
- package/dist/{chunk-HFOGKSNU.cjs.map → chunk-L3VJXZFD.cjs.map} +1 -1
- package/dist/{chunk-65IVT4GK.js → chunk-LTGF5OHP.js} +1008 -631
- package/dist/chunk-LTGF5OHP.js.map +1 -0
- package/dist/chunk-TWDLOXRM.cjs +1189 -0
- package/dist/chunk-TWDLOXRM.cjs.map +1 -0
- package/dist/{chunk-UXDWH2JC.cjs → chunk-VK3T6O5Z.cjs} +1165 -1118
- package/dist/chunk-VK3T6O5Z.cjs.map +1 -0
- package/dist/{chunk-CXL254BT.cjs → chunk-XY2GZUHC.cjs} +204 -18
- package/dist/chunk-XY2GZUHC.cjs.map +1 -0
- package/dist/{chunk-DDCTNLLD.cjs → chunk-ZMLQIMJK.cjs} +1504 -1128
- package/dist/chunk-ZMLQIMJK.cjs.map +1 -0
- package/dist/index.cjs +53 -1013
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -14
- package/dist/index.d.ts +22 -14
- package/dist/index.js +6 -993
- package/dist/index.js.map +1 -1
- package/dist/interaction/index.cjs +231 -231
- package/dist/interaction/index.cjs.map +1 -1
- package/dist/interaction/index.d.cts +2 -2
- package/dist/interaction/index.d.ts +2 -2
- package/dist/interaction/index.js +3 -3
- package/dist/interaction/index.js.map +1 -1
- package/dist/{render-C5X74hds.d.cts → render-CgX_y15a.d.cts} +193 -116
- package/dist/{render-C5X74hds.d.ts → render-CgX_y15a.d.ts} +193 -116
- package/dist/runtime-DnUxca3Y.d.ts +10 -0
- package/dist/runtime-reWgr7MP.d.cts +10 -0
- package/dist/{runtimeTypes-D8hPZ62T.d.ts → runtimeTypes-CEBGueYM.d.ts} +10 -5
- package/dist/{runtimeTypes-yTSAvtCn.d.cts → runtimeTypes-D60w6nyF.d.cts} +10 -5
- package/dist/stickyflow-diagram.global.js +35 -35
- package/dist/stickyflow-diagram.global.js.map +1 -1
- package/dist/{types-BDKDf4zr.d.cts → types-DYLC_lsk.d.ts} +3 -2
- package/dist/{types-CdgGdtoD.d.ts → types-DvhMpqrL.d.cts} +3 -2
- package/dist/viewer/index.cjs +9 -9
- package/dist/viewer/index.d.cts +4 -4
- package/dist/viewer/index.d.ts +4 -4
- package/dist/viewer/index.js +4 -4
- package/dist/web-component/index.cjs +6 -6
- package/dist/web-component/index.d.cts +2 -2
- package/dist/web-component/index.d.ts +2 -2
- package/dist/web-component/index.js +4 -4
- package/package.json +6 -1
- package/dist/chunk-65IVT4GK.js.map +0 -1
- package/dist/chunk-BYMIYXP5.js.map +0 -1
- package/dist/chunk-CSXQD3ON.js.map +0 -1
- package/dist/chunk-CXL254BT.cjs.map +0 -1
- package/dist/chunk-DDCTNLLD.cjs.map +0 -1
- package/dist/chunk-UXDWH2JC.cjs.map +0 -1
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
import { renderCanvas, sflToRenderInput } from './chunk-LTGF5OHP.js';
|
|
2
|
+
import { getComponentDescriptor, parseSource, isGenericComponent, isBuiltInComponentType } from './chunk-IEHE6WVN.js';
|
|
3
|
+
|
|
4
|
+
// src/binding/types.ts
|
|
5
|
+
var DECLARED = {
|
|
6
|
+
number: "number",
|
|
7
|
+
int: "number",
|
|
8
|
+
float: "number",
|
|
9
|
+
string: "string",
|
|
10
|
+
boolean: "boolean",
|
|
11
|
+
bool: "boolean"
|
|
12
|
+
};
|
|
13
|
+
var scalarOf = (declared) => DECLARED[declared];
|
|
14
|
+
var typeName = (type) => typeof type === "string" ? type : `list<${type.list}>`;
|
|
15
|
+
var sameType = (left, right) => typeName(left) === typeName(right);
|
|
16
|
+
var POINTER_EVENTS = ["onPress", "onRelease", "onTap", "onDrag"];
|
|
17
|
+
var CHANGE_EVENT = "onChange";
|
|
18
|
+
var TICK_EVENT = "onTick";
|
|
19
|
+
|
|
20
|
+
// src/binding/contract.ts
|
|
21
|
+
function identOf(value) {
|
|
22
|
+
return value?.type === "ident" ? value.segments.join(".") : void 0;
|
|
23
|
+
}
|
|
24
|
+
function readContract(block, diagnostics) {
|
|
25
|
+
const properties = block.entries.flatMap((entry) => entry.type === "property" ? [entry.property] : []);
|
|
26
|
+
const mode = identOf(properties.find((property) => property.key === "mode")?.value) === "record" ? "record" : "table";
|
|
27
|
+
const contract = { name: block.name, mode, fields: {}, declared: {} };
|
|
28
|
+
for (const entry of block.entries) {
|
|
29
|
+
if (entry.type === "fields") {
|
|
30
|
+
for (const field of entry.fields) {
|
|
31
|
+
const declared = identOf(field.value) ?? "";
|
|
32
|
+
const scalar = scalarOf(declared);
|
|
33
|
+
if (scalar) {
|
|
34
|
+
contract.fields[field.key] = scalar;
|
|
35
|
+
contract.declared[field.key] = declared;
|
|
36
|
+
} else {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
severity: "error",
|
|
39
|
+
code: "unknown-field-type",
|
|
40
|
+
message: `Field '${block.name}.${field.key}' has unknown type '${declared}' (use int, float, number, string or bool).`,
|
|
41
|
+
line: 0,
|
|
42
|
+
col: 0
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return contract;
|
|
49
|
+
}
|
|
50
|
+
function visitBlocks(blocks, visit) {
|
|
51
|
+
for (const block of blocks) {
|
|
52
|
+
visit(block);
|
|
53
|
+
visitBlocks(
|
|
54
|
+
block.entries.flatMap((entry) => entry.type === "block" ? [entry.block] : []),
|
|
55
|
+
visit
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function collectContracts(sflDocument) {
|
|
60
|
+
const contracts = /* @__PURE__ */ new Map();
|
|
61
|
+
const diagnostics = [];
|
|
62
|
+
visitBlocks(sflDocument.blocks, (block) => {
|
|
63
|
+
if (block.kind !== "DataSource") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (contracts.has(block.name)) {
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
severity: "error",
|
|
69
|
+
code: "duplicate-data-source",
|
|
70
|
+
message: `DataSource '${block.name}' is declared more than once.`,
|
|
71
|
+
line: 0,
|
|
72
|
+
col: 0
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
contracts.set(block.name, readContract(block, diagnostics));
|
|
77
|
+
});
|
|
78
|
+
return { contracts, diagnostics };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/binding/dataSource.ts
|
|
82
|
+
function createDataSource(initial) {
|
|
83
|
+
let current = initial;
|
|
84
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
85
|
+
return {
|
|
86
|
+
get: () => current,
|
|
87
|
+
set(next) {
|
|
88
|
+
current = next;
|
|
89
|
+
for (const listener of [...listeners]) {
|
|
90
|
+
listener();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
subscribe(listener) {
|
|
94
|
+
listeners.add(listener);
|
|
95
|
+
return () => listeners.delete(listener);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/binding/componentBinding.ts
|
|
101
|
+
var BINDABLE_COMPONENT_PROPERTIES = ["value", "disabled"];
|
|
102
|
+
function acceptsUserChange(componentType) {
|
|
103
|
+
return getComponentDescriptor(componentType)?.value?.writable === true;
|
|
104
|
+
}
|
|
105
|
+
function componentStateType(componentType, state) {
|
|
106
|
+
const descriptor = getComponentDescriptor(componentType);
|
|
107
|
+
if (state === "focused") {
|
|
108
|
+
return "boolean";
|
|
109
|
+
}
|
|
110
|
+
if (state === "pointer") {
|
|
111
|
+
return descriptor?.pointerScale ? "number" : void 0;
|
|
112
|
+
}
|
|
113
|
+
return state === "value" ? descriptor?.value?.type : void 0;
|
|
114
|
+
}
|
|
115
|
+
function componentPropertyType(componentType, property) {
|
|
116
|
+
if (property === "disabled") {
|
|
117
|
+
return "boolean";
|
|
118
|
+
}
|
|
119
|
+
return property === "value" ? getComponentDescriptor(componentType)?.value?.type : void 0;
|
|
120
|
+
}
|
|
121
|
+
function twoWayTargetProblem(key, expr, contracts) {
|
|
122
|
+
const target = expr.kind === "ref" ? expr.path : void 0;
|
|
123
|
+
const contract = target?.length === 2 ? contracts.get(target[0]) : void 0;
|
|
124
|
+
if (!target || contract?.mode !== "record" || !contract.fields[target[1]]) {
|
|
125
|
+
return `'<->' on '${key}' has to name one field of a record DataSource, as in <DataSource>.<field>.`;
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/binding/expression.ts
|
|
131
|
+
var isNumeric = (type) => type === "number" || typeof type === "object" && type.list === "number";
|
|
132
|
+
var isList = (type) => typeof type === "object";
|
|
133
|
+
function collectRefs(expr) {
|
|
134
|
+
switch (expr.kind) {
|
|
135
|
+
case "ref":
|
|
136
|
+
return [expr.path];
|
|
137
|
+
case "unary":
|
|
138
|
+
return collectRefs(expr.operand);
|
|
139
|
+
case "binary":
|
|
140
|
+
return [...collectRefs(expr.left), ...collectRefs(expr.right)];
|
|
141
|
+
case "call":
|
|
142
|
+
return expr.args.flatMap(collectRefs);
|
|
143
|
+
default:
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function inferBinary(expr, scope, report) {
|
|
148
|
+
const left = inferType(expr.left, scope, report);
|
|
149
|
+
const right = inferType(expr.right, scope, report);
|
|
150
|
+
if (!left || !right) {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
const { operator } = expr;
|
|
154
|
+
if (operator === "&&" || operator === "||") {
|
|
155
|
+
if (left !== "boolean" || right !== "boolean") {
|
|
156
|
+
report("type-mismatch", `'${operator}' needs two booleans, got ${typeName(left)} and ${typeName(right)}.`);
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
return "boolean";
|
|
160
|
+
}
|
|
161
|
+
if ([">", "<", ">=", "<="].includes(operator)) {
|
|
162
|
+
if (left !== "number" || right !== "number") {
|
|
163
|
+
report("type-mismatch", `'${operator}' compares two numbers, got ${typeName(left)} and ${typeName(right)}.`);
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
return "boolean";
|
|
167
|
+
}
|
|
168
|
+
if (operator === "==" || operator === "!=") {
|
|
169
|
+
if (isList(left) || !sameType(left, right)) {
|
|
170
|
+
report(
|
|
171
|
+
"type-mismatch",
|
|
172
|
+
`'${operator}' compares two values of the same simple type, got ${typeName(left)} and ${typeName(right)}.`
|
|
173
|
+
);
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
return "boolean";
|
|
177
|
+
}
|
|
178
|
+
if (operator === "+" && !isList(left) && !isList(right) && (left === "string" || right === "string")) {
|
|
179
|
+
return "string";
|
|
180
|
+
}
|
|
181
|
+
if (!isNumeric(left) || !isNumeric(right)) {
|
|
182
|
+
report("type-mismatch", `'${operator}' needs numbers, got ${typeName(left)} and ${typeName(right)}.`);
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
return isList(left) || isList(right) ? { list: "number" } : "number";
|
|
186
|
+
}
|
|
187
|
+
var MATH = {
|
|
188
|
+
round: { arguments: 1, apply: ([value]) => Math.round(value) },
|
|
189
|
+
floor: { arguments: 1, apply: ([value]) => Math.floor(value) },
|
|
190
|
+
ceil: { arguments: 1, apply: ([value]) => Math.ceil(value) },
|
|
191
|
+
abs: { arguments: 1, apply: ([value]) => Math.abs(value) },
|
|
192
|
+
min: { arguments: 2, apply: ([a, b]) => Math.min(a, b) },
|
|
193
|
+
max: { arguments: 2, apply: ([a, b]) => Math.max(a, b) },
|
|
194
|
+
clamp: { arguments: 3, apply: ([value, low, high]) => Math.min(high, Math.max(low, value)) },
|
|
195
|
+
moveToward: {
|
|
196
|
+
arguments: 3,
|
|
197
|
+
apply: ([current, target, maxStep]) => Math.abs(target - current) <= maxStep ? target : current + Math.sign(target - current) * maxStep
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function inferMath(name, expr, scope, report) {
|
|
201
|
+
const spec = MATH[name];
|
|
202
|
+
if (expr.args.length !== spec.arguments) {
|
|
203
|
+
report("wrong-arguments", `${name}() takes exactly ${spec.arguments} argument${spec.arguments === 1 ? "" : "s"}.`);
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
const types = expr.args.map((arg) => inferType(arg, scope, report));
|
|
207
|
+
if (types.some((type) => !type)) {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
if (types.some((type) => !isNumeric(type))) {
|
|
211
|
+
report(
|
|
212
|
+
"type-mismatch",
|
|
213
|
+
`${name}() needs numbers, got ${types.map((type) => typeName(type)).join(", ")}.`
|
|
214
|
+
);
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
return types.some((type) => isList(type)) ? { list: "number" } : "number";
|
|
218
|
+
}
|
|
219
|
+
function inferCall(expr, scope, report) {
|
|
220
|
+
const name = expr.callee.join(".");
|
|
221
|
+
if (name in MATH) {
|
|
222
|
+
return inferMath(name, expr, scope, report);
|
|
223
|
+
}
|
|
224
|
+
if (name !== "when") {
|
|
225
|
+
report("unknown-function", `Unknown function '${name}'. Available: when, ${Object.keys(MATH).join(", ")}.`);
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
if (expr.args.length !== 3) {
|
|
229
|
+
report("wrong-arguments", "when(condition, then, else) takes exactly three arguments.");
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
const [condition, whenTrue, whenFalse] = expr.args.map((arg) => inferType(arg, scope, report));
|
|
233
|
+
if (!condition || !whenTrue || !whenFalse) {
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
if (condition !== "boolean") {
|
|
237
|
+
report("type-mismatch", `The condition of when() must be boolean, got ${typeName(condition)}.`);
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
if (!sameType(whenTrue, whenFalse)) {
|
|
241
|
+
report(
|
|
242
|
+
"type-mismatch",
|
|
243
|
+
`when() branches must have the same type, got ${typeName(whenTrue)} and ${typeName(whenFalse)}.`
|
|
244
|
+
);
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
return whenTrue;
|
|
248
|
+
}
|
|
249
|
+
function inferType(expr, scope, report) {
|
|
250
|
+
switch (expr.kind) {
|
|
251
|
+
case "number":
|
|
252
|
+
return "number";
|
|
253
|
+
case "string":
|
|
254
|
+
return "string";
|
|
255
|
+
case "boolean":
|
|
256
|
+
return "boolean";
|
|
257
|
+
case "ref": {
|
|
258
|
+
const type = scope.resolve(expr.path);
|
|
259
|
+
if (!type) {
|
|
260
|
+
const name = expr.path.join(".");
|
|
261
|
+
report(
|
|
262
|
+
"unknown-reference",
|
|
263
|
+
name === "time.delta" ? "'time.delta' is only available in an onTick handler." : `Unknown reference '${name}'. Use <DataSource>.<field>, or <element>.pressed / hovered / pointer.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return type;
|
|
267
|
+
}
|
|
268
|
+
case "unary": {
|
|
269
|
+
const operand = inferType(expr.operand, scope, report);
|
|
270
|
+
const wanted = expr.operator === "!" ? "boolean" : "number";
|
|
271
|
+
if (operand && operand !== wanted) {
|
|
272
|
+
report("type-mismatch", `'${expr.operator}' needs a ${wanted}, got ${typeName(operand)}.`);
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
return operand ? wanted : void 0;
|
|
276
|
+
}
|
|
277
|
+
case "binary":
|
|
278
|
+
return inferBinary(expr, scope, report);
|
|
279
|
+
case "call":
|
|
280
|
+
return inferCall(expr, scope, report);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function arithmetic(operator, left, right) {
|
|
284
|
+
switch (operator) {
|
|
285
|
+
case "+":
|
|
286
|
+
return left + right;
|
|
287
|
+
case "-":
|
|
288
|
+
return left - right;
|
|
289
|
+
case "*":
|
|
290
|
+
return left * right;
|
|
291
|
+
default:
|
|
292
|
+
return right === 0 ? 0 : left / right;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var text = (value) => typeof value === "number" ? String(Math.round(value * 10) / 10) : String(value);
|
|
296
|
+
function evaluateBinary(expr, env) {
|
|
297
|
+
const left = evaluate(expr.left, env);
|
|
298
|
+
if (expr.operator === "&&") {
|
|
299
|
+
return Boolean(left) && Boolean(evaluate(expr.right, env));
|
|
300
|
+
}
|
|
301
|
+
if (expr.operator === "||") {
|
|
302
|
+
return Boolean(left) || Boolean(evaluate(expr.right, env));
|
|
303
|
+
}
|
|
304
|
+
const right = evaluate(expr.right, env);
|
|
305
|
+
switch (expr.operator) {
|
|
306
|
+
case ">":
|
|
307
|
+
return left > right;
|
|
308
|
+
case "<":
|
|
309
|
+
return left < right;
|
|
310
|
+
case ">=":
|
|
311
|
+
return left >= right;
|
|
312
|
+
case "<=":
|
|
313
|
+
return left <= right;
|
|
314
|
+
case "==":
|
|
315
|
+
return left === right;
|
|
316
|
+
case "!=":
|
|
317
|
+
return left !== right;
|
|
318
|
+
}
|
|
319
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
320
|
+
return text(left) + text(right);
|
|
321
|
+
}
|
|
322
|
+
const apply = (a, b) => arithmetic(expr.operator, a, b);
|
|
323
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
324
|
+
const length = Math.min(left.length, right.length);
|
|
325
|
+
return Array.from({ length }, (_value, index) => apply(left[index], right[index]));
|
|
326
|
+
}
|
|
327
|
+
if (Array.isArray(left)) {
|
|
328
|
+
return left.map((value) => apply(value, right));
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(right)) {
|
|
331
|
+
return right.map((value) => apply(left, value));
|
|
332
|
+
}
|
|
333
|
+
return apply(left, right);
|
|
334
|
+
}
|
|
335
|
+
function evaluate(expr, env) {
|
|
336
|
+
switch (expr.kind) {
|
|
337
|
+
case "number":
|
|
338
|
+
case "string":
|
|
339
|
+
case "boolean":
|
|
340
|
+
return expr.value;
|
|
341
|
+
case "ref":
|
|
342
|
+
return env.read(expr.path);
|
|
343
|
+
case "unary": {
|
|
344
|
+
const operand = evaluate(expr.operand, env);
|
|
345
|
+
return expr.operator === "!" ? !operand : -operand;
|
|
346
|
+
}
|
|
347
|
+
case "binary":
|
|
348
|
+
return evaluateBinary(expr, env);
|
|
349
|
+
case "call": {
|
|
350
|
+
const name = expr.callee.join(".");
|
|
351
|
+
if (name in MATH) {
|
|
352
|
+
const values = expr.args.map((arg) => evaluate(arg, env));
|
|
353
|
+
const length = Math.max(0, ...values.map((value) => Array.isArray(value) ? value.length : 0));
|
|
354
|
+
if (length === 0 && !values.some(Array.isArray)) {
|
|
355
|
+
return MATH[name].apply(values);
|
|
356
|
+
}
|
|
357
|
+
return Array.from(
|
|
358
|
+
{ length },
|
|
359
|
+
(_value, index) => MATH[name].apply(values.map((value) => Array.isArray(value) ? value[index] : value))
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const [condition, whenTrue, whenFalse] = expr.args;
|
|
363
|
+
return evaluate(condition, env) ? evaluate(whenTrue, env) : evaluate(whenFalse, env);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/binding/targets.ts
|
|
369
|
+
var BINDABLE_PROPERTIES = {
|
|
370
|
+
Chart: { categories: { list: "string" }, text: "string" },
|
|
371
|
+
Series: { values: { list: "number" }, label: "string" },
|
|
372
|
+
Shape: { text: "string", fill: "string", stroke: "string", textColor: "string", opacity: "number" },
|
|
373
|
+
Text: { text: "string", textColor: "string", opacity: "number" }
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/binding/graph.ts
|
|
377
|
+
function collectSites(blocks, chartPath, sites) {
|
|
378
|
+
for (const block of blocks) {
|
|
379
|
+
const path = block.kind === "Series" ? [...chartPath, block.name] : [block.name];
|
|
380
|
+
const ownChart = block.kind === "Chart" ? [block.name] : chartPath;
|
|
381
|
+
for (const entry of block.entries) {
|
|
382
|
+
if (entry.type === "binding") {
|
|
383
|
+
sites.push({ kind: blockKind(block), path, binding: entry.binding });
|
|
384
|
+
} else if (entry.type === "style") {
|
|
385
|
+
for (const binding of entry.style.bindings ?? []) {
|
|
386
|
+
sites.push({ kind: blockKind(block), path, binding });
|
|
387
|
+
}
|
|
388
|
+
} else if (entry.type === "block") {
|
|
389
|
+
collectSites([entry.block], ownChart, sites);
|
|
390
|
+
} else if (entry.type === "data") {
|
|
391
|
+
for (const inner of entry.data.entries) {
|
|
392
|
+
if (inner.type === "binding") {
|
|
393
|
+
sites.push({ kind: "Chart", path: ownChart, binding: inner.binding });
|
|
394
|
+
} else if (inner.type === "block") {
|
|
395
|
+
collectSites([inner.block], ownChart, sites);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function fieldType(contract, field) {
|
|
403
|
+
const type = contract.fields[field];
|
|
404
|
+
if (!type) {
|
|
405
|
+
return void 0;
|
|
406
|
+
}
|
|
407
|
+
return contract.mode === "table" ? { list: type } : type;
|
|
408
|
+
}
|
|
409
|
+
var INTERACTIVE_KINDS = ["Shape", "Text", "Component"];
|
|
410
|
+
var blockKind = (block) => block.kind === "Widget" ? "Component" : block.kind;
|
|
411
|
+
function collectElements(sflDocument) {
|
|
412
|
+
const elements = /* @__PURE__ */ new Map();
|
|
413
|
+
visitBlocks(sflDocument.blocks, (block) => {
|
|
414
|
+
if (!INTERACTIVE_KINDS.includes(blockKind(block)) || elements.has(block.name)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const type = block.entries.find((entry) => entry.type === "property" && entry.property.key === "type");
|
|
418
|
+
const value = type?.type === "property" ? type.property.value : void 0;
|
|
419
|
+
elements.set(block.name, {
|
|
420
|
+
kind: blockKind(block),
|
|
421
|
+
componentType: value?.type === "ident" ? value.segments.join(".") : void 0
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
return elements;
|
|
425
|
+
}
|
|
426
|
+
function elementStateType(element, state) {
|
|
427
|
+
if (state === "pressed" || state === "hovered") {
|
|
428
|
+
return "boolean";
|
|
429
|
+
}
|
|
430
|
+
return element.kind === "Component" ? componentStateType(element.componentType, state) : void 0;
|
|
431
|
+
}
|
|
432
|
+
function createScope(contracts, elements, ticking = false) {
|
|
433
|
+
return {
|
|
434
|
+
resolve(path) {
|
|
435
|
+
if (path.length !== 2) {
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
const contract = contracts.get(path[0]);
|
|
439
|
+
if (contract) {
|
|
440
|
+
return fieldType(contract, path[1]);
|
|
441
|
+
}
|
|
442
|
+
if (ticking && path[0] === "time" && path[1] === "delta") {
|
|
443
|
+
return "number";
|
|
444
|
+
}
|
|
445
|
+
const element = elements.get(path[0]);
|
|
446
|
+
return element ? elementStateType(element, path[1]) : void 0;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function qualify(expr, source) {
|
|
451
|
+
switch (expr.kind) {
|
|
452
|
+
case "ref":
|
|
453
|
+
return expr.path.length === 1 && expr.path[0] in source.fields ? { kind: "ref", path: [source.name, expr.path[0]] } : expr;
|
|
454
|
+
case "unary":
|
|
455
|
+
return { ...expr, operand: qualify(expr.operand, source) };
|
|
456
|
+
case "binary":
|
|
457
|
+
return { ...expr, left: qualify(expr.left, source), right: qualify(expr.right, source) };
|
|
458
|
+
case "call":
|
|
459
|
+
return { ...expr, args: expr.args.map((arg) => qualify(arg, source)) };
|
|
460
|
+
default:
|
|
461
|
+
return expr;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function collectHandlerSites(sflDocument) {
|
|
465
|
+
const sites = [];
|
|
466
|
+
visitBlocks(sflDocument.blocks, (block) => {
|
|
467
|
+
for (const entry of block.entries) {
|
|
468
|
+
if (entry.type === "handler") {
|
|
469
|
+
sites.push({ kind: blockKind(block), name: block.name, handler: entry.handler });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
return sites;
|
|
474
|
+
}
|
|
475
|
+
function allowedEvents(kind, componentType) {
|
|
476
|
+
if (kind === "DataSource") {
|
|
477
|
+
return [TICK_EVENT];
|
|
478
|
+
}
|
|
479
|
+
if (!INTERACTIVE_KINDS.includes(kind)) {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
const changes = kind === "Component" && acceptsUserChange(componentType);
|
|
483
|
+
return changes ? [...POINTER_EVENTS, CHANGE_EVENT] : POINTER_EVENTS;
|
|
484
|
+
}
|
|
485
|
+
function eventIsKnown(site, componentType, report) {
|
|
486
|
+
const { kind, name, handler } = site;
|
|
487
|
+
const allowed = allowedEvents(kind, componentType);
|
|
488
|
+
if (allowed.includes(handler.event)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
report(
|
|
492
|
+
"unknown-event",
|
|
493
|
+
allowed.length ? `${kind} '${name}' has no event '${handler.event}' (events: ${allowed.join(", ")}).` : `${kind} '${name}' has no events; handlers belong on a Shape, Text, Component or DataSource.`
|
|
494
|
+
);
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
function bindAssignment(assignment, name, own, contracts, scope, report) {
|
|
498
|
+
const at = (code, message) => report(code, message, assignment.line, assignment.col);
|
|
499
|
+
const target = assignment.target.join(".");
|
|
500
|
+
const [source, field] = own && assignment.target.length === 1 ? [own.name, assignment.target[0]] : assignment.target;
|
|
501
|
+
const contract = assignment.target.length <= 2 ? contracts.get(source) : void 0;
|
|
502
|
+
if (!contract || field === void 0) {
|
|
503
|
+
at("unknown-target", `'${target}' is not a field of a DataSource (write <DataSource>.<field>).`);
|
|
504
|
+
return void 0;
|
|
505
|
+
}
|
|
506
|
+
if (own && contract !== own) {
|
|
507
|
+
at("tick-foreign-target", `onTick of '${name}' can only change fields of '${name}', not '${target}'.`);
|
|
508
|
+
return void 0;
|
|
509
|
+
}
|
|
510
|
+
if (contract.mode !== "record") {
|
|
511
|
+
at(
|
|
512
|
+
"target-not-record",
|
|
513
|
+
`DataSource '${source}' is a table source; only record sources can be changed by a handler.`
|
|
514
|
+
);
|
|
515
|
+
return void 0;
|
|
516
|
+
}
|
|
517
|
+
const wanted = contract.fields[field];
|
|
518
|
+
if (!wanted) {
|
|
519
|
+
at("unknown-target", `DataSource '${source}' has no field '${field}'.`);
|
|
520
|
+
return void 0;
|
|
521
|
+
}
|
|
522
|
+
const expr = own ? qualify(assignment.expr, own) : assignment.expr;
|
|
523
|
+
const actual = inferType(expr, scope, (code, message) => at(code, message));
|
|
524
|
+
if (!actual) {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
if (!sameType(actual, wanted)) {
|
|
528
|
+
at("type-mismatch", `'${target}' is ${wanted}, the expression is ${typeName(actual)}.`);
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
return { source, field, expr, type: wanted, line: assignment.line, col: assignment.col };
|
|
532
|
+
}
|
|
533
|
+
function bindHandler(site, contracts, elements, diagnostics) {
|
|
534
|
+
const { kind, name, handler } = site;
|
|
535
|
+
const report = (code, message, line = handler.line, col = handler.col) => diagnostics.push({ severity: "error", code, message, line, col });
|
|
536
|
+
if (!eventIsKnown(site, elements.get(name)?.componentType, report)) {
|
|
537
|
+
return void 0;
|
|
538
|
+
}
|
|
539
|
+
const ticking = kind === "DataSource";
|
|
540
|
+
const own = ticking ? contracts.get(name) : void 0;
|
|
541
|
+
if (ticking && own?.mode !== "record") {
|
|
542
|
+
report("tick-needs-record", `DataSource '${name}' has to be a record source to change its fields on every tick.`);
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
const scope = createScope(contracts, elements, ticking);
|
|
546
|
+
const assignments = handler.assignments.flatMap(
|
|
547
|
+
(assignment) => bindAssignment(assignment, name, own, contracts, scope, report) ?? []
|
|
548
|
+
);
|
|
549
|
+
return {
|
|
550
|
+
id: `${kind}.${name}.${handler.event}`,
|
|
551
|
+
ownerKind: kind,
|
|
552
|
+
ownerPath: [name],
|
|
553
|
+
event: handler.event,
|
|
554
|
+
assignments,
|
|
555
|
+
line: handler.line,
|
|
556
|
+
col: handler.col
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function warnNameCollisions(contracts, elements, diagnostics) {
|
|
560
|
+
for (const name of elements.keys()) {
|
|
561
|
+
if (contracts.has(name)) {
|
|
562
|
+
diagnostics.push({
|
|
563
|
+
severity: "warning",
|
|
564
|
+
code: "name-collision",
|
|
565
|
+
message: `'${name}' is a DataSource and an element; in expressions it means the DataSource.`,
|
|
566
|
+
line: 0,
|
|
567
|
+
col: 0
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function expectedType(site, elements) {
|
|
573
|
+
const { kind, path, binding } = site;
|
|
574
|
+
if (kind === "Shader") {
|
|
575
|
+
return "number";
|
|
576
|
+
}
|
|
577
|
+
if (kind === "Component") {
|
|
578
|
+
return componentPropertyType(elements.get(path[0])?.componentType, binding.key);
|
|
579
|
+
}
|
|
580
|
+
return BINDABLE_PROPERTIES[kind]?.[binding.key];
|
|
581
|
+
}
|
|
582
|
+
function canBindTwoWay(site, elements) {
|
|
583
|
+
const { kind, path, binding } = site;
|
|
584
|
+
return kind === "Component" && binding.key === "value" && acceptsUserChange(elements.get(path[0])?.componentType);
|
|
585
|
+
}
|
|
586
|
+
function rejectedOperator(site, elements, contracts) {
|
|
587
|
+
const { binding } = site;
|
|
588
|
+
const twoWay = binding.operator === "<->";
|
|
589
|
+
if (binding.operator === "->" || twoWay && !canBindTwoWay(site, elements)) {
|
|
590
|
+
return {
|
|
591
|
+
code: "binding-operator-unsupported",
|
|
592
|
+
message: `'${binding.operator}' on '${binding.key}' is parsed but not executed; '<->' works on the value of a component that the user can change, and '->' is not supported.`,
|
|
593
|
+
severity: "warning"
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const problem = twoWay ? twoWayTargetProblem(binding.key, binding.expr, contracts) : void 0;
|
|
597
|
+
return problem ? { code: "two-way-target", message: problem, severity: "error" } : void 0;
|
|
598
|
+
}
|
|
599
|
+
function bindProperty(site, scope, elements, contracts, diagnostics) {
|
|
600
|
+
const { kind, path, binding } = site;
|
|
601
|
+
const owner = path.join(".");
|
|
602
|
+
const report = (code, message, severity = "error") => diagnostics.push({ severity, code, message, line: binding.line, col: binding.col });
|
|
603
|
+
const rejected = rejectedOperator(site, elements, contracts);
|
|
604
|
+
if (rejected) {
|
|
605
|
+
report(rejected.code, rejected.message, rejected.severity);
|
|
606
|
+
return void 0;
|
|
607
|
+
}
|
|
608
|
+
const expected = expectedType(site, elements);
|
|
609
|
+
if (!expected) {
|
|
610
|
+
const allowed = kind === "Component" ? BINDABLE_COMPONENT_PROPERTIES : Object.keys(BINDABLE_PROPERTIES[kind] ?? {});
|
|
611
|
+
report(
|
|
612
|
+
"unknown-bound-property",
|
|
613
|
+
`${kind} '${owner}' has no bindable property '${binding.key}'${allowed.length ? ` (bindable: ${allowed.join(", ")})` : ""}.`
|
|
614
|
+
);
|
|
615
|
+
return void 0;
|
|
616
|
+
}
|
|
617
|
+
const actual = inferType(binding.expr, scope, report);
|
|
618
|
+
if (!actual) {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
if (!sameType(actual, expected)) {
|
|
622
|
+
report("type-mismatch", `'${binding.key}' expects ${typeName(expected)}, the expression is ${typeName(actual)}.`);
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
id: `${kind}.${owner}.${binding.key}`,
|
|
627
|
+
ownerKind: kind,
|
|
628
|
+
ownerPath: path,
|
|
629
|
+
property: binding.key,
|
|
630
|
+
operator: binding.operator,
|
|
631
|
+
expr: binding.expr,
|
|
632
|
+
type: expected,
|
|
633
|
+
dependencies: [...new Set(collectRefs(binding.expr).map((ref) => ref.join(".")))],
|
|
634
|
+
line: binding.line,
|
|
635
|
+
col: binding.col
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function indexByField(bindings) {
|
|
639
|
+
const byField = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const binding of bindings) {
|
|
641
|
+
for (const dependency of binding.dependencies) {
|
|
642
|
+
byField.set(dependency, [...byField.get(dependency) ?? [], binding]);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return byField;
|
|
646
|
+
}
|
|
647
|
+
function buildBindingGraph(sflDocument) {
|
|
648
|
+
const { contracts, diagnostics } = collectContracts(sflDocument);
|
|
649
|
+
const elements = collectElements(sflDocument);
|
|
650
|
+
warnNameCollisions(contracts, elements, diagnostics);
|
|
651
|
+
const scope = createScope(contracts, elements);
|
|
652
|
+
const sites = [];
|
|
653
|
+
collectSites(sflDocument.blocks, [], sites);
|
|
654
|
+
const bindings = sites.flatMap((site) => bindProperty(site, scope, elements, contracts, diagnostics) ?? []);
|
|
655
|
+
const handlers = collectHandlerSites(sflDocument).flatMap(
|
|
656
|
+
(site) => bindHandler(site, contracts, elements, diagnostics) ?? []
|
|
657
|
+
);
|
|
658
|
+
return { contracts, bindings, handlers, elements, byField: indexByField(bindings), diagnostics };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/binding/interactive.ts
|
|
662
|
+
var OPERATED = /* @__PURE__ */ new Set(["toggle", "press"]);
|
|
663
|
+
function interactiveElements(graph) {
|
|
664
|
+
const pressable = /* @__PURE__ */ new Set();
|
|
665
|
+
const scales = /* @__PURE__ */ new Set();
|
|
666
|
+
const note = ([name, state]) => {
|
|
667
|
+
if (graph.contracts.has(name) || !graph.elements.has(name)) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
(state === "pointer" ? scales : pressable).add(name);
|
|
671
|
+
};
|
|
672
|
+
for (const handler of graph.handlers) {
|
|
673
|
+
if (handler.ownerKind !== "DataSource") {
|
|
674
|
+
pressable.add(handler.ownerPath[0]);
|
|
675
|
+
}
|
|
676
|
+
for (const assignment of handler.assignments) {
|
|
677
|
+
collectRefs(assignment.expr).forEach(note);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
for (const binding of graph.bindings) {
|
|
681
|
+
collectRefs(binding.expr).forEach(note);
|
|
682
|
+
}
|
|
683
|
+
for (const [name, info] of graph.elements) {
|
|
684
|
+
const interaction = getComponentDescriptor(info.componentType)?.interaction;
|
|
685
|
+
if (info.kind === "Component" && interaction && OPERATED.has(interaction) && !graph.contracts.has(name)) {
|
|
686
|
+
pressable.add(name);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return { pressable, scales };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/binding/checkData.ts
|
|
693
|
+
var typeOfValue = (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? typeof value : "other";
|
|
694
|
+
function checkData(contract, data) {
|
|
695
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
696
|
+
if (contract.mode === "table" !== Array.isArray(data)) {
|
|
697
|
+
return `DataSource '${contract.name}' is a ${contract.mode} source but delivered ${Array.isArray(data) ? "rows" : "a record"}.`;
|
|
698
|
+
}
|
|
699
|
+
for (const row of rows) {
|
|
700
|
+
for (const [field, type] of Object.entries(contract.fields)) {
|
|
701
|
+
if (typeOfValue(row[field]) !== type) {
|
|
702
|
+
return `'${contract.name}.${field}' should be ${contract.declared[field]}, got ${typeOfValue(row[field])}.`;
|
|
703
|
+
}
|
|
704
|
+
if (contract.declared[field] === "int" && !Number.isInteger(row[field])) {
|
|
705
|
+
return `'${contract.name}.${field}' should be a whole number (int), got ${String(row[field])}.`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/binding/applyValue.ts
|
|
713
|
+
var findChart = (input, id) => input.charts?.find((candidate) => candidate.id === id);
|
|
714
|
+
function applyToSeries(input, path, change) {
|
|
715
|
+
const chart = findChart(input, path[0]);
|
|
716
|
+
if (!chart) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
let series = chart.series.find((candidate) => candidate.id === path[1]);
|
|
720
|
+
if (!series) {
|
|
721
|
+
series = { id: path[1], label: path[1], values: [] };
|
|
722
|
+
chart.series.push(series);
|
|
723
|
+
}
|
|
724
|
+
change(series);
|
|
725
|
+
}
|
|
726
|
+
function findComponent(input, id) {
|
|
727
|
+
return (input.components ?? input.widgets)?.find((candidate) => candidate.id === id);
|
|
728
|
+
}
|
|
729
|
+
function writeComponentValue(input, id, value) {
|
|
730
|
+
const component = findComponent(input, id);
|
|
731
|
+
if (!component) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
component.value = isGenericComponent(component) ? value : String(value);
|
|
735
|
+
}
|
|
736
|
+
function writeComponentDisabled(input, id, disabled) {
|
|
737
|
+
const component = findComponent(input, id);
|
|
738
|
+
if (component && isGenericComponent(component)) {
|
|
739
|
+
component.disabled = disabled;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function isComponentDisabled(input, id) {
|
|
743
|
+
const component = findComponent(input, id);
|
|
744
|
+
return component !== void 0 && isGenericComponent(component) && component.disabled;
|
|
745
|
+
}
|
|
746
|
+
function setShape(input, path, change) {
|
|
747
|
+
const shape = input.shapes.find((candidate) => candidate.id === path[0]);
|
|
748
|
+
if (shape) {
|
|
749
|
+
change(shape);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
var APPLY = {
|
|
753
|
+
"Chart.categories": (input, path, value) => {
|
|
754
|
+
const chart = findChart(input, path[0]);
|
|
755
|
+
if (chart) {
|
|
756
|
+
chart.config = { ...chart.config ?? {}, categories: value };
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
"Chart.text": (input, path, value) => {
|
|
760
|
+
const chart = findChart(input, path[0]);
|
|
761
|
+
if (chart) {
|
|
762
|
+
chart.title = value;
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
"Series.values": (input, path, value) => applyToSeries(input, path, (series) => series.values = value),
|
|
766
|
+
"Series.label": (input, path, value) => applyToSeries(input, path, (series) => series.label = value),
|
|
767
|
+
"Shape.text": (input, path, value) => {
|
|
768
|
+
const shape = input.shapes.find((candidate) => candidate.id === path[0]);
|
|
769
|
+
if (shape) {
|
|
770
|
+
shape.text = value;
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
"Shape.fill": (input, path, value) => setShape(input, path, (shape) => shape.fillColor = value),
|
|
774
|
+
"Shape.stroke": (input, path, value) => setShape(input, path, (shape) => shape.strokeColor = value),
|
|
775
|
+
"Shape.textColor": (input, path, value) => setShape(input, path, (shape) => shape.textColor = value),
|
|
776
|
+
"Shape.opacity": (input, path, value) => setShape(input, path, (shape) => shape.opacity = value),
|
|
777
|
+
"Component.value": (input, path, value) => writeComponentValue(input, path[0], value),
|
|
778
|
+
"Component.disabled": (input, path, value) => writeComponentDisabled(input, path[0], value === true)
|
|
779
|
+
};
|
|
780
|
+
function applyShader(input, binding, value) {
|
|
781
|
+
const owners = [];
|
|
782
|
+
for (const shape of input.shapes) {
|
|
783
|
+
if (shape.shader?.name === binding.ownerPath[0]) {
|
|
784
|
+
shape.shader.parameters = { ...shape.shader.parameters, [binding.property]: value };
|
|
785
|
+
owners.push(shape.id);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return owners;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/binding/runtimeState.ts
|
|
792
|
+
function initialValue(input, id, componentType) {
|
|
793
|
+
const descriptor = getComponentDescriptor(componentType);
|
|
794
|
+
const current = findComponent(input, id)?.value;
|
|
795
|
+
if (current === void 0) {
|
|
796
|
+
return descriptor?.value?.default ?? 0;
|
|
797
|
+
}
|
|
798
|
+
if (descriptor?.value?.type === "number") {
|
|
799
|
+
const value = Number(current);
|
|
800
|
+
return Number.isFinite(value) ? value : descriptor.value.default;
|
|
801
|
+
}
|
|
802
|
+
return current;
|
|
803
|
+
}
|
|
804
|
+
function createRuntimeState(sfl, options) {
|
|
805
|
+
const graph = buildBindingGraph(parseSource(sfl));
|
|
806
|
+
const input = structuredClone(sflToRenderInput(sfl));
|
|
807
|
+
const elementState = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const [name, info] of graph.elements) {
|
|
809
|
+
if (!graph.contracts.has(name)) {
|
|
810
|
+
elementState.set(name, {
|
|
811
|
+
pressed: false,
|
|
812
|
+
hovered: false,
|
|
813
|
+
focused: false,
|
|
814
|
+
pointer: 0,
|
|
815
|
+
value: initialValue(input, name, info.componentType)
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
options,
|
|
821
|
+
graph,
|
|
822
|
+
diagnostics: [...graph.diagnostics],
|
|
823
|
+
input,
|
|
824
|
+
sources: /* @__PURE__ */ new Map(),
|
|
825
|
+
applied: /* @__PURE__ */ new Map(),
|
|
826
|
+
dirty: /* @__PURE__ */ new Set(),
|
|
827
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
828
|
+
warned: /* @__PURE__ */ new Set(),
|
|
829
|
+
liveListeners: /* @__PURE__ */ new Set(),
|
|
830
|
+
liveState: /* @__PURE__ */ new Map(),
|
|
831
|
+
elementState,
|
|
832
|
+
scheduled: false,
|
|
833
|
+
renderNeeded: false
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function readValue(state, path) {
|
|
837
|
+
const [name, field] = path;
|
|
838
|
+
const element = state.sources.has(name) ? void 0 : state.elementState.get(name);
|
|
839
|
+
if (element) {
|
|
840
|
+
return element[field];
|
|
841
|
+
}
|
|
842
|
+
const data = state.sources.get(name)?.get();
|
|
843
|
+
if (data === void 0) {
|
|
844
|
+
throw new Error(`DataSource '${name}' has no data yet.`);
|
|
845
|
+
}
|
|
846
|
+
return Array.isArray(data) ? data.map((row) => row[field]) : data[field];
|
|
847
|
+
}
|
|
848
|
+
function missingSource(state, binding) {
|
|
849
|
+
return binding.dependencies.map((dependency) => dependency.split(".")[0]).find((name) => !state.sources.has(name) && !state.elementState.has(name));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/binding/evaluation.ts
|
|
853
|
+
function announce(state, id, change) {
|
|
854
|
+
state.liveState.set(id, change);
|
|
855
|
+
for (const listener of [...state.liveListeners]) {
|
|
856
|
+
listener(change);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function applyComponentValue(state, binding, value) {
|
|
860
|
+
const id = binding.ownerPath[0];
|
|
861
|
+
const element = state.elementState.get(id);
|
|
862
|
+
if (element) {
|
|
863
|
+
element.value = value;
|
|
864
|
+
markDirty(state, id);
|
|
865
|
+
}
|
|
866
|
+
if (!isBuiltInComponentType(state.graph.elements.get(id)?.componentType)) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
announce(state, binding.id, { kind: "component", id, value });
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
function warnMissingSource(state, binding, missing) {
|
|
873
|
+
if (state.warned.has(missing)) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
state.warned.add(missing);
|
|
877
|
+
state.diagnostics.push({
|
|
878
|
+
severity: "warning",
|
|
879
|
+
code: "source-not-registered",
|
|
880
|
+
message: `DataSource '${missing}' has no registered source, so '${binding.property}' stays empty.`,
|
|
881
|
+
line: binding.line,
|
|
882
|
+
col: binding.col
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function evaluateBinding(state, binding) {
|
|
886
|
+
const missing = missingSource(state, binding);
|
|
887
|
+
if (missing) {
|
|
888
|
+
warnMissingSource(state, binding, missing);
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
const value = evaluate(binding.expr, { read: (path) => readValue(state, path) });
|
|
892
|
+
const key = JSON.stringify(value);
|
|
893
|
+
if (state.applied.get(binding.id) === key) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
state.applied.set(binding.id, key);
|
|
897
|
+
if (binding.ownerKind === "Shader") {
|
|
898
|
+
announce(state, binding.id, {
|
|
899
|
+
kind: "shader",
|
|
900
|
+
owners: applyShader(state.input, binding, value),
|
|
901
|
+
parameters: { [binding.property]: value }
|
|
902
|
+
});
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
APPLY[`${binding.ownerKind === "Text" ? "Shape" : binding.ownerKind}.${binding.property}`]?.(
|
|
906
|
+
state.input,
|
|
907
|
+
binding.ownerPath,
|
|
908
|
+
value
|
|
909
|
+
);
|
|
910
|
+
if (binding.ownerKind === "Component" && binding.property === "value") {
|
|
911
|
+
return applyComponentValue(state, binding, value);
|
|
912
|
+
}
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
function flush(state) {
|
|
916
|
+
state.scheduled = false;
|
|
917
|
+
let changed = state.renderNeeded;
|
|
918
|
+
state.renderNeeded = false;
|
|
919
|
+
for (const binding of state.dirty) {
|
|
920
|
+
changed = evaluateBinding(state, binding) || changed;
|
|
921
|
+
}
|
|
922
|
+
state.dirty.clear();
|
|
923
|
+
if (!changed) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const result = renderCanvas(state.input, { ...state.options.renderOptions, linkHitAreas: true });
|
|
927
|
+
for (const listener of [...state.listeners]) {
|
|
928
|
+
listener(result);
|
|
929
|
+
}
|
|
930
|
+
return result;
|
|
931
|
+
}
|
|
932
|
+
function schedule(state) {
|
|
933
|
+
if (!state.scheduled) {
|
|
934
|
+
state.scheduled = true;
|
|
935
|
+
queueMicrotask(() => flush(state));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function markDirty(state, name) {
|
|
939
|
+
for (const [field, bindings] of state.graph.byField) {
|
|
940
|
+
if (field.startsWith(`${name}.`)) {
|
|
941
|
+
for (const binding of bindings) {
|
|
942
|
+
state.dirty.add(binding);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/binding/handlers.ts
|
|
949
|
+
function runHandler(state, handler, delta) {
|
|
950
|
+
const env = {
|
|
951
|
+
read: (path) => path[0] === "time" && path[1] === "delta" ? delta ?? 0 : readValue(state, path)
|
|
952
|
+
};
|
|
953
|
+
for (const assignment of handler.assignments) {
|
|
954
|
+
const target = state.sources.get(assignment.source);
|
|
955
|
+
const readable = collectRefs(assignment.expr).every(
|
|
956
|
+
([name]) => name === "time" || state.sources.has(name) || state.elementState.has(name)
|
|
957
|
+
);
|
|
958
|
+
if (!target || !("set" in target) || !readable) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
let value = evaluate(assignment.expr, env);
|
|
962
|
+
if (typeof value === "number") {
|
|
963
|
+
value = state.graph.contracts.get(assignment.source)?.declared[assignment.field] === "int" ? Math.round(value) : value;
|
|
964
|
+
if (!Number.isFinite(value)) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const row = target.get();
|
|
969
|
+
if (row[assignment.field] !== value) {
|
|
970
|
+
target.set({ ...row, [assignment.field]: value });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function dispatch(state, id, event) {
|
|
975
|
+
for (const handler of state.graph.handlers) {
|
|
976
|
+
if (handler.event === event && handler.ownerKind !== "DataSource" && handler.ownerPath[0] === id) {
|
|
977
|
+
runHandler(state, handler);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function runTick(state, seconds) {
|
|
982
|
+
for (const handler of state.graph.handlers) {
|
|
983
|
+
if (handler.event === TICK_EVENT) {
|
|
984
|
+
runHandler(state, handler, seconds);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function setElementState(state, id, patch) {
|
|
989
|
+
const current = state.elementState.get(id);
|
|
990
|
+
if (!current || Object.keys(patch).every((key) => current[key] === patch[key])) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
Object.assign(current, patch);
|
|
994
|
+
markDirty(state, id);
|
|
995
|
+
schedule(state);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/binding/userInput.ts
|
|
999
|
+
function descriptorOf(state, id) {
|
|
1000
|
+
const info = state.graph.elements.get(id);
|
|
1001
|
+
return info?.kind === "Component" ? getComponentDescriptor(info.componentType) : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
function valueBinding(state, id) {
|
|
1004
|
+
return state.graph.bindings.find(
|
|
1005
|
+
(binding) => binding.ownerKind === "Component" && binding.ownerPath[0] === id && binding.property === "value"
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
function writeBack(state, binding, value) {
|
|
1009
|
+
const target = binding.expr.kind === "ref" ? binding.expr.path : void 0;
|
|
1010
|
+
const source = target ? state.sources.get(target[0]) : void 0;
|
|
1011
|
+
if (!target || !source || !("set" in source)) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const row = source.get();
|
|
1015
|
+
if (row[target[1]] !== value) {
|
|
1016
|
+
source.set({ ...row, [target[1]]: value });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function changeComponentValue(state, id, next) {
|
|
1020
|
+
const spec = descriptorOf(state, id)?.value;
|
|
1021
|
+
const element = state.elementState.get(id);
|
|
1022
|
+
if (!spec?.writable || !element || typeof next !== spec.type || element.value === next) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
if (isComponentDisabled(state.input, id)) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
const binding = valueBinding(state, id);
|
|
1029
|
+
if (binding && binding.operator !== "<->") {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
element.value = next;
|
|
1033
|
+
writeComponentValue(state.input, id, next);
|
|
1034
|
+
state.renderNeeded = true;
|
|
1035
|
+
if (binding) {
|
|
1036
|
+
state.applied.set(binding.id, JSON.stringify(next));
|
|
1037
|
+
writeBack(state, binding, next);
|
|
1038
|
+
}
|
|
1039
|
+
markDirty(state, id);
|
|
1040
|
+
dispatch(state, id, CHANGE_EVENT);
|
|
1041
|
+
schedule(state);
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
function reactToActivation(state, id) {
|
|
1045
|
+
const element = state.elementState.get(id);
|
|
1046
|
+
const descriptor = descriptorOf(state, id);
|
|
1047
|
+
if (element && descriptor?.interaction === "toggle" && descriptor.value?.type === "boolean") {
|
|
1048
|
+
changeComponentValue(state, id, element.value !== true);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function pressElement(state, id) {
|
|
1052
|
+
if (isComponentDisabled(state.input, id)) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
setElementState(state, id, { pressed: true });
|
|
1056
|
+
dispatch(state, id, "onPress");
|
|
1057
|
+
}
|
|
1058
|
+
function releaseElement(state, id, tapped) {
|
|
1059
|
+
setElementState(state, id, { pressed: false });
|
|
1060
|
+
if (isComponentDisabled(state.input, id)) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (tapped) {
|
|
1064
|
+
reactToActivation(state, id);
|
|
1065
|
+
}
|
|
1066
|
+
dispatch(state, id, "onRelease");
|
|
1067
|
+
if (tapped) {
|
|
1068
|
+
dispatch(state, id, "onTap");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function activateElement(state, id) {
|
|
1072
|
+
if (isComponentDisabled(state.input, id)) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
reactToActivation(state, id);
|
|
1076
|
+
dispatch(state, id, "onTap");
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/binding/runtime.ts
|
|
1080
|
+
function registerSource(state, name, source) {
|
|
1081
|
+
const contract = state.graph.contracts.get(name);
|
|
1082
|
+
if (!contract) {
|
|
1083
|
+
throw new Error(`No DataSource '${name}' is declared in the SFL.`);
|
|
1084
|
+
}
|
|
1085
|
+
const accept = () => {
|
|
1086
|
+
const problem = checkData(contract, source.get());
|
|
1087
|
+
if (problem) {
|
|
1088
|
+
state.diagnostics.push({ severity: "error", code: "data-type-mismatch", message: problem, line: 0, col: 0 });
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
return true;
|
|
1092
|
+
};
|
|
1093
|
+
const adopt = () => {
|
|
1094
|
+
if (accept()) {
|
|
1095
|
+
state.sources.set(name, source);
|
|
1096
|
+
state.warned.delete(name);
|
|
1097
|
+
markDirty(state, name);
|
|
1098
|
+
schedule(state);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
adopt();
|
|
1102
|
+
const unsubscribe = source.subscribe(adopt);
|
|
1103
|
+
return () => {
|
|
1104
|
+
unsubscribe();
|
|
1105
|
+
state.sources.delete(name);
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function pushLiveChange(target, change) {
|
|
1109
|
+
if (change.kind === "component") {
|
|
1110
|
+
(target.setComponentValue ?? target.setWidgetValue)?.call(target, change.id, change.value);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
for (const owner of change.owners) {
|
|
1114
|
+
target.setShaderParameters?.(owner, change.parameters);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function attach(state, target) {
|
|
1118
|
+
const send = (change) => pushLiveChange(target, change);
|
|
1119
|
+
state.liveListeners.add(send);
|
|
1120
|
+
for (const change of state.liveState.values()) {
|
|
1121
|
+
send(change);
|
|
1122
|
+
}
|
|
1123
|
+
const listener = (result) => target.update({ svg: result.svg });
|
|
1124
|
+
state.listeners.add(listener);
|
|
1125
|
+
for (const binding of state.graph.bindings) {
|
|
1126
|
+
state.dirty.add(binding);
|
|
1127
|
+
}
|
|
1128
|
+
flush(state);
|
|
1129
|
+
return () => {
|
|
1130
|
+
state.listeners.delete(listener);
|
|
1131
|
+
state.liveListeners.delete(send);
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function createBindingRuntime(sfl, options = {}) {
|
|
1135
|
+
const state = createRuntimeState(sfl, options);
|
|
1136
|
+
for (const binding of state.graph.bindings) {
|
|
1137
|
+
if (binding.dependencies.length === 0) {
|
|
1138
|
+
state.dirty.add(binding);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (state.dirty.size > 0) {
|
|
1142
|
+
schedule(state);
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
diagnostics: state.diagnostics,
|
|
1146
|
+
graph: state.graph,
|
|
1147
|
+
interactive: interactiveElements(state.graph),
|
|
1148
|
+
input: state.input,
|
|
1149
|
+
registerSource: (name, source) => registerSource(state, name, source),
|
|
1150
|
+
setElementState: (id, patch) => setElementState(state, id, patch),
|
|
1151
|
+
press: (id) => pressElement(state, id),
|
|
1152
|
+
release: (id, tapped) => releaseElement(state, id, tapped),
|
|
1153
|
+
activate: (id) => activateElement(state, id),
|
|
1154
|
+
change: (id, value) => changeComponentValue(state, id, value),
|
|
1155
|
+
drag(id, pointer) {
|
|
1156
|
+
if (pointer !== void 0) {
|
|
1157
|
+
setElementState(state, id, { pointer });
|
|
1158
|
+
}
|
|
1159
|
+
dispatch(state, id, "onDrag");
|
|
1160
|
+
},
|
|
1161
|
+
tick: (seconds) => runTick(state, seconds),
|
|
1162
|
+
flush: () => flush(state),
|
|
1163
|
+
onRender(listener) {
|
|
1164
|
+
state.listeners.add(listener);
|
|
1165
|
+
return () => state.listeners.delete(listener);
|
|
1166
|
+
},
|
|
1167
|
+
attach: (target) => attach(state, target)
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function renderBound(sfl, options = {}) {
|
|
1171
|
+
const { data = {}, ...renderOptions } = options;
|
|
1172
|
+
const runtime = createBindingRuntime(sfl, { renderOptions });
|
|
1173
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
1174
|
+
runtime.registerSource(name, { get: () => rows, subscribe: () => () => void 0 });
|
|
1175
|
+
}
|
|
1176
|
+
runtime.flush();
|
|
1177
|
+
return renderCanvas(runtime.input, renderOptions);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
export { buildBindingGraph, collectContracts, createBindingRuntime, createDataSource, interactiveElements, renderBound };
|
|
1181
|
+
//# sourceMappingURL=chunk-BODZN3ZI.js.map
|
|
1182
|
+
//# sourceMappingURL=chunk-BODZN3ZI.js.map
|