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