@skygraph/core 0.0.0-placeholder.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/LICENSE +21 -0
- package/README.md +42 -6
- package/dist/calendar.cjs +404 -0
- package/dist/calendar.cjs.map +1 -0
- package/dist/calendar.d.cts +209 -0
- package/dist/calendar.d.ts +209 -0
- package/dist/calendar.js +12 -0
- package/dist/calendar.js.map +1 -0
- package/dist/chunk-5CCD5Q4B.js +36 -0
- package/dist/chunk-5CCD5Q4B.js.map +1 -0
- package/dist/chunk-7FA2TBSZ.js +1211 -0
- package/dist/chunk-7FA2TBSZ.js.map +1 -0
- package/dist/chunk-AB3RLBLW.js +357 -0
- package/dist/chunk-AB3RLBLW.js.map +1 -0
- package/dist/chunk-GEMALROQ.js +258 -0
- package/dist/chunk-GEMALROQ.js.map +1 -0
- package/dist/chunk-IY6LJU3L.js +256 -0
- package/dist/chunk-IY6LJU3L.js.map +1 -0
- package/dist/chunk-KMGRNPLG.js +453 -0
- package/dist/chunk-KMGRNPLG.js.map +1 -0
- package/dist/chunk-Y7FTBBYX.js +397 -0
- package/dist/chunk-Y7FTBBYX.js.map +1 -0
- package/dist/chunk-ZWB7JGAJ.js +488 -0
- package/dist/chunk-ZWB7JGAJ.js.map +1 -0
- package/dist/form.cjs +498 -0
- package/dist/form.cjs.map +1 -0
- package/dist/form.d.cts +130 -0
- package/dist/form.d.ts +130 -0
- package/dist/form.js +8 -0
- package/dist/form.js.map +1 -0
- package/dist/graph.cjs +1269 -0
- package/dist/graph.cjs.map +1 -0
- package/dist/graph.d.cts +671 -0
- package/dist/graph.d.ts +671 -0
- package/dist/graph.js +34 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.cjs +3860 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +477 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-internal.cjs +339 -0
- package/dist/runtime-internal.cjs.map +1 -0
- package/dist/runtime-internal.d.cts +130 -0
- package/dist/runtime-internal.d.ts +130 -0
- package/dist/runtime-internal.js +67 -0
- package/dist/runtime-internal.js.map +1 -0
- package/dist/table.cjs +537 -0
- package/dist/table.cjs.map +1 -0
- package/dist/table.d.cts +215 -0
- package/dist/table.d.ts +215 -0
- package/dist/table.js +16 -0
- package/dist/table.js.map +1 -0
- package/dist/tree.cjs +442 -0
- package/dist/tree.cjs.map +1 -0
- package/dist/tree.d.cts +76 -0
- package/dist/tree.d.ts +76 -0
- package/dist/tree.js +8 -0
- package/dist/tree.js.map +1 -0
- package/dist/types-B-rKQJ4R.d.cts +35 -0
- package/dist/types-B-rKQJ4R.d.ts +35 -0
- package/dist/virtual.cjs +283 -0
- package/dist/virtual.cjs.map +1 -0
- package/dist/virtual.d.cts +96 -0
- package/dist/virtual.d.ts +96 -0
- package/dist/virtual.js +9 -0
- package/dist/virtual.js.map +1 -0
- package/package.json +98 -18
- package/index.js +0 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3860 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key2 of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key2) && key2 !== except)
|
|
14
|
+
__defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
CALENDAR_PREFIX: () => CALENDAR_PREFIX,
|
|
24
|
+
FORM_META_PREFIX: () => FORM_META_PREFIX,
|
|
25
|
+
GRAPH_PREFIX: () => GRAPH_PREFIX,
|
|
26
|
+
TABLE_PREFIX: () => TABLE_PREFIX,
|
|
27
|
+
TREE_PREFIX: () => TREE_PREFIX,
|
|
28
|
+
aabbFromOBB: () => aabbFromOBB,
|
|
29
|
+
computeAggregate: () => computeAggregate,
|
|
30
|
+
createCalendar: () => createCalendar,
|
|
31
|
+
createCore: () => createCore,
|
|
32
|
+
createForm: () => createForm,
|
|
33
|
+
createGraph: () => createGraph,
|
|
34
|
+
createHistory: () => createHistory,
|
|
35
|
+
createMeasureCache: () => createMeasureCache,
|
|
36
|
+
createTable: () => createTable,
|
|
37
|
+
createTree: () => createTree,
|
|
38
|
+
createTypedCore: () => createTypedCore,
|
|
39
|
+
createVirtual: () => createVirtual,
|
|
40
|
+
detectConflicts: () => detectConflicts,
|
|
41
|
+
floatingAnchor: () => floatingAnchor,
|
|
42
|
+
freezeMiddleware: () => freezeMiddleware,
|
|
43
|
+
getBezierPath: () => getBezierPath,
|
|
44
|
+
getEdgePosition: () => getEdgePosition,
|
|
45
|
+
getNodeIntersection: () => getNodeIntersection,
|
|
46
|
+
inferSide: () => inferSide,
|
|
47
|
+
isAvailable: () => isAvailable,
|
|
48
|
+
listReservedPrefixes: () => listReservedPrefixes,
|
|
49
|
+
loggerMiddleware: () => loggerMiddleware,
|
|
50
|
+
matchesColumnFilter: () => matchesColumnFilter,
|
|
51
|
+
matchesOperator: () => matchesOperator,
|
|
52
|
+
nearestSide: () => nearestSide,
|
|
53
|
+
obbContainsPoint: () => obbContainsPoint,
|
|
54
|
+
obbCorners: () => obbCorners,
|
|
55
|
+
persistenceMiddleware: () => persistenceMiddleware,
|
|
56
|
+
pointsToPath: () => pointsToPath,
|
|
57
|
+
pointsToRoundedPath: () => pointsToRoundedPath,
|
|
58
|
+
reservePrefix: () => reservePrefix,
|
|
59
|
+
resolveEdgeEndpoint: () => resolveEdgeEndpoint,
|
|
60
|
+
resolveOperator: () => resolveOperator,
|
|
61
|
+
routeOrthogonal: () => routeOrthogonal,
|
|
62
|
+
validationMiddleware: () => validationMiddleware
|
|
63
|
+
});
|
|
64
|
+
module.exports = __toCommonJS(src_exports);
|
|
65
|
+
|
|
66
|
+
// src/runtime/Node.ts
|
|
67
|
+
function createNode(id, value) {
|
|
68
|
+
return { id, value, version: 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/runtime/Store.ts
|
|
72
|
+
var Store = class {
|
|
73
|
+
nodes = /* @__PURE__ */ new Map();
|
|
74
|
+
has(path) {
|
|
75
|
+
return this.nodes.has(path);
|
|
76
|
+
}
|
|
77
|
+
get(path) {
|
|
78
|
+
return this.nodes.get(path)?.value;
|
|
79
|
+
}
|
|
80
|
+
getNode(path) {
|
|
81
|
+
return this.nodes.get(path);
|
|
82
|
+
}
|
|
83
|
+
set(path, value) {
|
|
84
|
+
let node = this.nodes.get(path);
|
|
85
|
+
if (node) {
|
|
86
|
+
node.value = value;
|
|
87
|
+
node.version++;
|
|
88
|
+
return node;
|
|
89
|
+
}
|
|
90
|
+
node = createNode(path, value);
|
|
91
|
+
this.nodes.set(path, node);
|
|
92
|
+
return node;
|
|
93
|
+
}
|
|
94
|
+
delete(path) {
|
|
95
|
+
return this.nodes.delete(path);
|
|
96
|
+
}
|
|
97
|
+
paths() {
|
|
98
|
+
return this.nodes.keys();
|
|
99
|
+
}
|
|
100
|
+
size() {
|
|
101
|
+
return this.nodes.size;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/runtime/SubscriptionEngine.ts
|
|
106
|
+
var SubscriptionEngine = class {
|
|
107
|
+
listeners = /* @__PURE__ */ new Map();
|
|
108
|
+
subscribe(path, cb) {
|
|
109
|
+
let set = this.listeners.get(path);
|
|
110
|
+
if (!set) {
|
|
111
|
+
set = /* @__PURE__ */ new Set();
|
|
112
|
+
this.listeners.set(path, set);
|
|
113
|
+
}
|
|
114
|
+
set.add(cb);
|
|
115
|
+
return () => {
|
|
116
|
+
set.delete(cb);
|
|
117
|
+
if (set.size === 0) {
|
|
118
|
+
this.listeners.delete(path);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
notify(path, value) {
|
|
123
|
+
const set = this.listeners.get(path);
|
|
124
|
+
if (!set) return;
|
|
125
|
+
for (const cb of set) {
|
|
126
|
+
cb(value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
notifyMany(changes) {
|
|
130
|
+
for (const [path, value] of changes) {
|
|
131
|
+
this.notify(path, value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
hasListeners(path) {
|
|
135
|
+
const set = this.listeners.get(path);
|
|
136
|
+
return set !== void 0 && set.size > 0;
|
|
137
|
+
}
|
|
138
|
+
clear() {
|
|
139
|
+
this.listeners.clear();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/runtime/Batch.ts
|
|
144
|
+
var Batch = class {
|
|
145
|
+
depth = 0;
|
|
146
|
+
pending = /* @__PURE__ */ new Map();
|
|
147
|
+
get isActive() {
|
|
148
|
+
return this.depth > 0;
|
|
149
|
+
}
|
|
150
|
+
start() {
|
|
151
|
+
this.depth++;
|
|
152
|
+
}
|
|
153
|
+
end() {
|
|
154
|
+
this.depth--;
|
|
155
|
+
if (this.depth > 0) return null;
|
|
156
|
+
if (this.pending.size === 0) return null;
|
|
157
|
+
const changes = new Map(this.pending);
|
|
158
|
+
this.pending.clear();
|
|
159
|
+
return changes;
|
|
160
|
+
}
|
|
161
|
+
enqueue(path, value) {
|
|
162
|
+
this.pending.set(path, value);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/runtime/Transaction.ts
|
|
167
|
+
var Transaction = class {
|
|
168
|
+
active = false;
|
|
169
|
+
staged = [];
|
|
170
|
+
get isActive() {
|
|
171
|
+
return this.active;
|
|
172
|
+
}
|
|
173
|
+
start() {
|
|
174
|
+
if (this.active) {
|
|
175
|
+
throw new Error("SkyGraph: nested transactions are not supported");
|
|
176
|
+
}
|
|
177
|
+
this.active = true;
|
|
178
|
+
this.staged = [];
|
|
179
|
+
}
|
|
180
|
+
/** STAGE: record a write without mutating the store */
|
|
181
|
+
stage(path, value, prevValue) {
|
|
182
|
+
this.staged.push({ path, value, prevValue });
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* RESOLVE + COMMIT: drop unchanged values, apply the rest to store.
|
|
186
|
+
* Returns the map of actually changed paths and their new values.
|
|
187
|
+
*/
|
|
188
|
+
commit(store) {
|
|
189
|
+
const changes = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const write of this.staged) {
|
|
191
|
+
if (Object.is(write.value, write.prevValue)) continue;
|
|
192
|
+
store.set(write.path, write.value);
|
|
193
|
+
changes.set(write.path, write.value);
|
|
194
|
+
}
|
|
195
|
+
this.active = false;
|
|
196
|
+
this.staged = [];
|
|
197
|
+
return changes;
|
|
198
|
+
}
|
|
199
|
+
getStaged() {
|
|
200
|
+
return this.staged;
|
|
201
|
+
}
|
|
202
|
+
complete() {
|
|
203
|
+
this.active = false;
|
|
204
|
+
this.staged = [];
|
|
205
|
+
}
|
|
206
|
+
rollback() {
|
|
207
|
+
this.active = false;
|
|
208
|
+
this.staged = [];
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/runtime/CycleDetector.ts
|
|
213
|
+
var CycleDetector = class {
|
|
214
|
+
graph = /* @__PURE__ */ new Map();
|
|
215
|
+
addEdge(from, to) {
|
|
216
|
+
let deps = this.graph.get(from);
|
|
217
|
+
if (!deps) {
|
|
218
|
+
deps = /* @__PURE__ */ new Set();
|
|
219
|
+
this.graph.set(from, deps);
|
|
220
|
+
}
|
|
221
|
+
deps.add(to);
|
|
222
|
+
if (this.hasCycle(to, /* @__PURE__ */ new Set([from]))) {
|
|
223
|
+
deps.delete(to);
|
|
224
|
+
if (deps.size === 0) this.graph.delete(from);
|
|
225
|
+
throw new Error(
|
|
226
|
+
`SkyGraph: circular dependency detected: ${from} -> ${to}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
removeNode(id) {
|
|
231
|
+
this.graph.delete(id);
|
|
232
|
+
for (const deps of this.graph.values()) {
|
|
233
|
+
deps.delete(id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
getDependents(id) {
|
|
237
|
+
const targets = this.graph.get(id);
|
|
238
|
+
return targets ? [...targets] : [];
|
|
239
|
+
}
|
|
240
|
+
hasCycle(node, visited) {
|
|
241
|
+
if (visited.has(node)) return true;
|
|
242
|
+
const deps = this.graph.get(node);
|
|
243
|
+
if (!deps) return false;
|
|
244
|
+
visited.add(node);
|
|
245
|
+
for (const dep of deps) {
|
|
246
|
+
if (this.hasCycle(dep, new Set(visited))) return true;
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/runtime/ComputedEngine.ts
|
|
253
|
+
var ComputedEngine = class {
|
|
254
|
+
computedNodes = /* @__PURE__ */ new Map();
|
|
255
|
+
cycleDetector = new CycleDetector();
|
|
256
|
+
register(target, deps, fn) {
|
|
257
|
+
if (this.computedNodes.has(target)) {
|
|
258
|
+
this.remove(target);
|
|
259
|
+
}
|
|
260
|
+
for (const dep of deps) {
|
|
261
|
+
this.cycleDetector.addEdge(dep, target);
|
|
262
|
+
}
|
|
263
|
+
this.computedNodes.set(target, { target, deps, fn });
|
|
264
|
+
}
|
|
265
|
+
remove(target) {
|
|
266
|
+
this.computedNodes.delete(target);
|
|
267
|
+
this.cycleDetector.removeNode(target);
|
|
268
|
+
}
|
|
269
|
+
isComputed(path) {
|
|
270
|
+
return this.computedNodes.has(path);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Like propagate, but returns both new and old values for middleware support.
|
|
274
|
+
* Does NOT write to the store — the caller handles that via dispatchWrite.
|
|
275
|
+
*/
|
|
276
|
+
propagateRaw(changedPaths, store) {
|
|
277
|
+
const result = /* @__PURE__ */ new Map();
|
|
278
|
+
const visited = /* @__PURE__ */ new Set();
|
|
279
|
+
const queue = [...changedPaths];
|
|
280
|
+
while (queue.length > 0) {
|
|
281
|
+
const path = queue.shift();
|
|
282
|
+
if (visited.has(path)) continue;
|
|
283
|
+
visited.add(path);
|
|
284
|
+
const dependents = this.cycleDetector.getDependents(path);
|
|
285
|
+
for (const depTarget of dependents) {
|
|
286
|
+
const computed = this.computedNodes.get(depTarget);
|
|
287
|
+
if (!computed) continue;
|
|
288
|
+
const depValues = computed.deps.map((d) => {
|
|
289
|
+
const pending = result.get(d);
|
|
290
|
+
return pending ? pending.value : store.get(d);
|
|
291
|
+
});
|
|
292
|
+
const newValue = computed.fn(...depValues);
|
|
293
|
+
const oldValue = result.has(depTarget) ? result.get(depTarget).oldValue : store.get(depTarget);
|
|
294
|
+
if (!Object.is(newValue, oldValue)) {
|
|
295
|
+
result.set(depTarget, { value: newValue, oldValue });
|
|
296
|
+
queue.push(depTarget);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Initialize all computed values based on current store state.
|
|
304
|
+
*/
|
|
305
|
+
initializeAll(store) {
|
|
306
|
+
for (const computed of this.computedNodes.values()) {
|
|
307
|
+
const depValues = computed.deps.map((d) => store.get(d));
|
|
308
|
+
const value = computed.fn(...depValues);
|
|
309
|
+
store.set(computed.target, value);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/Core.ts
|
|
315
|
+
var CoreImpl = class {
|
|
316
|
+
store = new Store();
|
|
317
|
+
subscriptions = new SubscriptionEngine();
|
|
318
|
+
batch_ = new Batch();
|
|
319
|
+
transaction_ = new Transaction();
|
|
320
|
+
computed_ = new ComputedEngine();
|
|
321
|
+
middlewares = [];
|
|
322
|
+
// ── Middleware ──
|
|
323
|
+
use(middleware) {
|
|
324
|
+
this.middlewares.push(middleware);
|
|
325
|
+
return () => {
|
|
326
|
+
const idx = this.middlewares.indexOf(middleware);
|
|
327
|
+
if (idx !== -1) this.middlewares.splice(idx, 1);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
dispatchWrite(event) {
|
|
331
|
+
let idx = 0;
|
|
332
|
+
const chain = (ev) => {
|
|
333
|
+
if (idx < this.middlewares.length) {
|
|
334
|
+
const mw = this.middlewares[idx++];
|
|
335
|
+
mw(ev, chain);
|
|
336
|
+
} else {
|
|
337
|
+
this.commitWrite(ev);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
chain(event);
|
|
341
|
+
}
|
|
342
|
+
commitWrite(event) {
|
|
343
|
+
this.store.set(event.path, event.value);
|
|
344
|
+
}
|
|
345
|
+
// ── Public API ──
|
|
346
|
+
get(path) {
|
|
347
|
+
return this.store.get(path);
|
|
348
|
+
}
|
|
349
|
+
set(path, value) {
|
|
350
|
+
if (this.transaction_.isActive) {
|
|
351
|
+
this.transaction_.stage(path, value, this.store.get(path));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const oldValue = this.store.get(path);
|
|
355
|
+
if (Object.is(oldValue, value)) return;
|
|
356
|
+
this.dispatchWrite({ path, value, oldValue, source: "user" });
|
|
357
|
+
const computedChanges = this.propagateComputed(/* @__PURE__ */ new Set([path]));
|
|
358
|
+
if (this.batch_.isActive) {
|
|
359
|
+
this.batch_.enqueue(path, value);
|
|
360
|
+
for (const [cp, cv] of computedChanges) {
|
|
361
|
+
this.batch_.enqueue(cp, cv);
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.subscriptions.notify(path, value);
|
|
366
|
+
this.subscriptions.notifyMany(computedChanges);
|
|
367
|
+
}
|
|
368
|
+
subscribe(path, cb) {
|
|
369
|
+
return this.subscriptions.subscribe(path, cb);
|
|
370
|
+
}
|
|
371
|
+
batch(fn) {
|
|
372
|
+
this.batch_.start();
|
|
373
|
+
try {
|
|
374
|
+
fn();
|
|
375
|
+
} finally {
|
|
376
|
+
const changes = this.batch_.end();
|
|
377
|
+
if (changes) {
|
|
378
|
+
this.subscriptions.notifyMany(changes);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
transaction(fn) {
|
|
383
|
+
this.batch_.start();
|
|
384
|
+
this.transaction_.start();
|
|
385
|
+
try {
|
|
386
|
+
fn();
|
|
387
|
+
const staged = this.transaction_.getStaged();
|
|
388
|
+
const approved = [];
|
|
389
|
+
const origCommit = this.commitWrite.bind(this);
|
|
390
|
+
this.commitWrite = (ev) => {
|
|
391
|
+
approved.push({ path: ev.path, value: ev.value });
|
|
392
|
+
};
|
|
393
|
+
try {
|
|
394
|
+
for (const write of staged) {
|
|
395
|
+
if (Object.is(write.value, write.prevValue)) continue;
|
|
396
|
+
this.dispatchWrite({
|
|
397
|
+
path: write.path,
|
|
398
|
+
value: write.value,
|
|
399
|
+
oldValue: write.prevValue,
|
|
400
|
+
source: "transaction"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} finally {
|
|
404
|
+
this.commitWrite = origCommit;
|
|
405
|
+
}
|
|
406
|
+
const changes = /* @__PURE__ */ new Map();
|
|
407
|
+
for (const { path, value } of approved) {
|
|
408
|
+
this.store.set(path, value);
|
|
409
|
+
changes.set(path, value);
|
|
410
|
+
}
|
|
411
|
+
this.transaction_.complete();
|
|
412
|
+
const computedChanges = this.propagateComputed(new Set(changes.keys()));
|
|
413
|
+
for (const [path, value] of changes) {
|
|
414
|
+
this.batch_.enqueue(path, value);
|
|
415
|
+
}
|
|
416
|
+
for (const [path, value] of computedChanges) {
|
|
417
|
+
this.batch_.enqueue(path, value);
|
|
418
|
+
}
|
|
419
|
+
} catch (e) {
|
|
420
|
+
this.transaction_.rollback();
|
|
421
|
+
throw e;
|
|
422
|
+
} finally {
|
|
423
|
+
const batchedChanges = this.batch_.end();
|
|
424
|
+
if (batchedChanges) {
|
|
425
|
+
this.subscriptions.notifyMany(batchedChanges);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
computed(target, deps, fn) {
|
|
430
|
+
this.computed_.register(target, deps, fn);
|
|
431
|
+
const depValues = deps.map((d) => this.store.get(d));
|
|
432
|
+
const value = fn(...depValues);
|
|
433
|
+
const oldValue = this.store.get(target);
|
|
434
|
+
this.dispatchWrite({
|
|
435
|
+
path: target,
|
|
436
|
+
value,
|
|
437
|
+
oldValue,
|
|
438
|
+
source: "computed"
|
|
439
|
+
});
|
|
440
|
+
if (!Object.is(oldValue, value)) {
|
|
441
|
+
this.subscriptions.notify(target, value);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ── Snapshot / Restore ──
|
|
445
|
+
snapshot() {
|
|
446
|
+
const result = {};
|
|
447
|
+
for (const path of this.store.paths()) {
|
|
448
|
+
result[path] = this.store.get(path);
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
restore(snap2) {
|
|
453
|
+
this.batch_.start();
|
|
454
|
+
try {
|
|
455
|
+
for (const [path, value] of Object.entries(snap2)) {
|
|
456
|
+
const oldValue = this.store.get(path);
|
|
457
|
+
if (!Object.is(oldValue, value)) {
|
|
458
|
+
this.dispatchWrite({ path, value, oldValue, source: "restore" });
|
|
459
|
+
this.batch_.enqueue(path, value);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} finally {
|
|
463
|
+
const changes = this.batch_.end();
|
|
464
|
+
if (changes) {
|
|
465
|
+
this.subscriptions.notifyMany(changes);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// ── Internal helpers ──
|
|
470
|
+
propagateComputed(changedPaths) {
|
|
471
|
+
const rawChanges = this.computed_.propagateRaw(changedPaths, this.store);
|
|
472
|
+
const dispatched = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const [path, { value, oldValue }] of rawChanges) {
|
|
474
|
+
this.dispatchWrite({ path, value, oldValue, source: "computed" });
|
|
475
|
+
dispatched.set(path, value);
|
|
476
|
+
}
|
|
477
|
+
return dispatched;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
function createCore() {
|
|
481
|
+
return new CoreImpl();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/middleware/logger.ts
|
|
485
|
+
function loggerMiddleware(options = {}) {
|
|
486
|
+
const log = options.log ?? ((msg, ev) => console.log(msg, ev.path, ev.value));
|
|
487
|
+
const filter = options.filter;
|
|
488
|
+
return (event, next) => {
|
|
489
|
+
if (filter && !filter(event)) {
|
|
490
|
+
next(event);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
log(`[skygraph:${event.source}]`, event);
|
|
494
|
+
next(event);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/middleware/freeze.ts
|
|
499
|
+
function freezeMiddleware(frozenPaths) {
|
|
500
|
+
return (event, next) => {
|
|
501
|
+
for (const prefix of frozenPaths) {
|
|
502
|
+
if (event.path === prefix || event.path.startsWith(prefix + ".")) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
next(event);
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/middleware/persistence.ts
|
|
511
|
+
function persistenceMiddleware(options = {}) {
|
|
512
|
+
const storageKey = options.key ?? "skygraph:state";
|
|
513
|
+
const storage = options.storage ?? (typeof localStorage !== "undefined" ? localStorage : null);
|
|
514
|
+
const paths = options.paths;
|
|
515
|
+
const debounceMs = options.debounce ?? 100;
|
|
516
|
+
let pending = {};
|
|
517
|
+
let timer = null;
|
|
518
|
+
function flush() {
|
|
519
|
+
if (!storage) return;
|
|
520
|
+
const existing = storage.getItem(storageKey);
|
|
521
|
+
let data = {};
|
|
522
|
+
if (existing) {
|
|
523
|
+
try {
|
|
524
|
+
data = JSON.parse(existing);
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
Object.assign(data, pending);
|
|
529
|
+
storage.setItem(storageKey, JSON.stringify(data));
|
|
530
|
+
pending = {};
|
|
531
|
+
timer = null;
|
|
532
|
+
}
|
|
533
|
+
return (event, next) => {
|
|
534
|
+
next(event);
|
|
535
|
+
if (paths && !paths.some((p) => event.path === p || event.path.startsWith(p + "."))) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
pending[event.path] = event.value;
|
|
539
|
+
if (timer === null) {
|
|
540
|
+
timer = setTimeout(flush, debounceMs);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/middleware/validation.ts
|
|
546
|
+
function validationMiddleware(optionsOrValidators) {
|
|
547
|
+
const isOptions = "validators" in optionsOrValidators;
|
|
548
|
+
const validators = isOptions ? optionsOrValidators.validators : optionsOrValidators;
|
|
549
|
+
const onReject = isOptions ? optionsOrValidators.onReject : void 0;
|
|
550
|
+
return (event, next) => {
|
|
551
|
+
const validator = validators[event.path];
|
|
552
|
+
if (validator) {
|
|
553
|
+
const result = validator(event);
|
|
554
|
+
if (result === false || typeof result === "string") {
|
|
555
|
+
onReject?.(event, typeof result === "string" ? result : "Validation failed");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
next(event);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/plugins/history.ts
|
|
564
|
+
function createHistory(core, options = {}) {
|
|
565
|
+
const maxEntries = options.maxEntries ?? 100;
|
|
566
|
+
const groupWindow = options.groupWindow ?? 0;
|
|
567
|
+
const filter = options.filter;
|
|
568
|
+
const entries = [];
|
|
569
|
+
let cursor = -1;
|
|
570
|
+
let travelling = false;
|
|
571
|
+
let groupTimer = null;
|
|
572
|
+
let pendingPatches = [];
|
|
573
|
+
function flushGroup() {
|
|
574
|
+
if (pendingPatches.length === 0) return;
|
|
575
|
+
const entry = {
|
|
576
|
+
timestamp: Date.now(),
|
|
577
|
+
patches: [...pendingPatches]
|
|
578
|
+
};
|
|
579
|
+
pendingPatches = [];
|
|
580
|
+
if (cursor < entries.length - 1) {
|
|
581
|
+
entries.splice(cursor + 1);
|
|
582
|
+
}
|
|
583
|
+
entries.push(entry);
|
|
584
|
+
cursor++;
|
|
585
|
+
if (entries.length > maxEntries) {
|
|
586
|
+
entries.shift();
|
|
587
|
+
cursor--;
|
|
588
|
+
}
|
|
589
|
+
groupTimer = null;
|
|
590
|
+
}
|
|
591
|
+
const middleware = (event, next) => {
|
|
592
|
+
next(event);
|
|
593
|
+
if (travelling) return;
|
|
594
|
+
if (event.source === "computed") return;
|
|
595
|
+
if (filter && !filter(event)) return;
|
|
596
|
+
pendingPatches.push({
|
|
597
|
+
path: event.path,
|
|
598
|
+
value: event.value,
|
|
599
|
+
oldValue: event.oldValue
|
|
600
|
+
});
|
|
601
|
+
if (groupWindow > 0) {
|
|
602
|
+
if (groupTimer === null) {
|
|
603
|
+
groupTimer = setTimeout(flushGroup, groupWindow);
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
flushGroup();
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const unsub = core.use(middleware);
|
|
610
|
+
function applyPatches(patches, direction) {
|
|
611
|
+
travelling = true;
|
|
612
|
+
try {
|
|
613
|
+
core.batch(() => {
|
|
614
|
+
if (direction === "undo") {
|
|
615
|
+
for (let i = patches.length - 1; i >= 0; i--) {
|
|
616
|
+
core.set(patches[i].path, patches[i].oldValue);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
for (const patch of patches) {
|
|
620
|
+
core.set(patch.path, patch.value);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
} finally {
|
|
625
|
+
travelling = false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
get entries() {
|
|
630
|
+
return entries;
|
|
631
|
+
},
|
|
632
|
+
get cursor() {
|
|
633
|
+
return cursor;
|
|
634
|
+
},
|
|
635
|
+
get canUndo() {
|
|
636
|
+
return cursor >= 0;
|
|
637
|
+
},
|
|
638
|
+
get canRedo() {
|
|
639
|
+
return cursor < entries.length - 1;
|
|
640
|
+
},
|
|
641
|
+
undo() {
|
|
642
|
+
if (cursor < 0) return;
|
|
643
|
+
flushGroup();
|
|
644
|
+
applyPatches(entries[cursor].patches, "undo");
|
|
645
|
+
cursor--;
|
|
646
|
+
},
|
|
647
|
+
redo() {
|
|
648
|
+
if (cursor >= entries.length - 1) return;
|
|
649
|
+
flushGroup();
|
|
650
|
+
cursor++;
|
|
651
|
+
applyPatches(entries[cursor].patches, "redo");
|
|
652
|
+
},
|
|
653
|
+
jumpTo(index) {
|
|
654
|
+
if (index < -1 || index >= entries.length) return;
|
|
655
|
+
flushGroup();
|
|
656
|
+
while (cursor > index) {
|
|
657
|
+
applyPatches(entries[cursor].patches, "undo");
|
|
658
|
+
cursor--;
|
|
659
|
+
}
|
|
660
|
+
while (cursor < index) {
|
|
661
|
+
cursor++;
|
|
662
|
+
applyPatches(entries[cursor].patches, "redo");
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
clear() {
|
|
666
|
+
entries.length = 0;
|
|
667
|
+
cursor = -1;
|
|
668
|
+
pendingPatches = [];
|
|
669
|
+
if (groupTimer !== null) {
|
|
670
|
+
clearTimeout(groupTimer);
|
|
671
|
+
groupTimer = null;
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
destroy() {
|
|
675
|
+
this.clear();
|
|
676
|
+
unsub();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/engines/virtual/measure.ts
|
|
682
|
+
function createMeasureCache(options) {
|
|
683
|
+
let itemCount = options.itemCount;
|
|
684
|
+
let estimate = options.estimate;
|
|
685
|
+
const measured = /* @__PURE__ */ new Map();
|
|
686
|
+
let offsets = null;
|
|
687
|
+
let cachedTotal = 0;
|
|
688
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
689
|
+
const getEstimate = (index) => typeof estimate === "number" ? estimate : estimate(index);
|
|
690
|
+
const getItemSize = (index) => {
|
|
691
|
+
const m = measured.get(index);
|
|
692
|
+
return m !== void 0 ? m : getEstimate(index);
|
|
693
|
+
};
|
|
694
|
+
const rebuildOffsets = () => {
|
|
695
|
+
if (offsets && offsets.length === itemCount) return;
|
|
696
|
+
const arr = new Array(itemCount);
|
|
697
|
+
let acc = 0;
|
|
698
|
+
for (let i = 0; i < itemCount; i++) {
|
|
699
|
+
arr[i] = acc;
|
|
700
|
+
acc += getItemSize(i);
|
|
701
|
+
}
|
|
702
|
+
offsets = arr;
|
|
703
|
+
cachedTotal = acc;
|
|
704
|
+
};
|
|
705
|
+
const getTotalHeight = () => {
|
|
706
|
+
rebuildOffsets();
|
|
707
|
+
return cachedTotal;
|
|
708
|
+
};
|
|
709
|
+
const getItemOffset = (index) => {
|
|
710
|
+
if (index <= 0) return 0;
|
|
711
|
+
if (index >= itemCount) return getTotalHeight();
|
|
712
|
+
rebuildOffsets();
|
|
713
|
+
return offsets[index];
|
|
714
|
+
};
|
|
715
|
+
const getItemAtOffset = (offset) => {
|
|
716
|
+
if (itemCount === 0) return 0;
|
|
717
|
+
rebuildOffsets();
|
|
718
|
+
let lo = 0;
|
|
719
|
+
let hi = itemCount - 1;
|
|
720
|
+
while (lo < hi) {
|
|
721
|
+
const mid = lo + hi + 1 >>> 1;
|
|
722
|
+
if (offsets[mid] <= offset) lo = mid;
|
|
723
|
+
else hi = mid - 1;
|
|
724
|
+
}
|
|
725
|
+
return lo;
|
|
726
|
+
};
|
|
727
|
+
const invalidate = (event) => {
|
|
728
|
+
offsets = null;
|
|
729
|
+
for (const l of listeners) l(event);
|
|
730
|
+
};
|
|
731
|
+
return {
|
|
732
|
+
get itemCount() {
|
|
733
|
+
return itemCount;
|
|
734
|
+
},
|
|
735
|
+
get measuredCount() {
|
|
736
|
+
return measured.size;
|
|
737
|
+
},
|
|
738
|
+
setItemCount(count) {
|
|
739
|
+
if (count === itemCount) return;
|
|
740
|
+
if (count < itemCount && measured.size > 0) {
|
|
741
|
+
for (const k of [...measured.keys()]) {
|
|
742
|
+
if (k >= count) measured.delete(k);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
itemCount = count;
|
|
746
|
+
invalidate("resize");
|
|
747
|
+
},
|
|
748
|
+
setEstimate(e) {
|
|
749
|
+
estimate = e;
|
|
750
|
+
invalidate("reset");
|
|
751
|
+
},
|
|
752
|
+
setMeasuredHeight(index, height) {
|
|
753
|
+
if (index < 0 || index >= itemCount) return false;
|
|
754
|
+
if (!Number.isFinite(height) || height < 0) return false;
|
|
755
|
+
const prev = measured.get(index);
|
|
756
|
+
if (prev === height) return false;
|
|
757
|
+
measured.set(index, height);
|
|
758
|
+
invalidate("measure");
|
|
759
|
+
return true;
|
|
760
|
+
},
|
|
761
|
+
clearMeasured(index) {
|
|
762
|
+
const had = measured.delete(index);
|
|
763
|
+
if (had) invalidate("measure");
|
|
764
|
+
return had;
|
|
765
|
+
},
|
|
766
|
+
reset() {
|
|
767
|
+
if (measured.size === 0) return;
|
|
768
|
+
measured.clear();
|
|
769
|
+
invalidate("reset");
|
|
770
|
+
},
|
|
771
|
+
hasMeasured(index) {
|
|
772
|
+
return measured.has(index);
|
|
773
|
+
},
|
|
774
|
+
getMeasuredHeight(index) {
|
|
775
|
+
return measured.get(index);
|
|
776
|
+
},
|
|
777
|
+
getEstimatedSize: getEstimate,
|
|
778
|
+
getItemSize,
|
|
779
|
+
getItemOffset,
|
|
780
|
+
getItemAtOffset,
|
|
781
|
+
getTotalHeight,
|
|
782
|
+
subscribe(listener) {
|
|
783
|
+
listeners.add(listener);
|
|
784
|
+
return () => {
|
|
785
|
+
listeners.delete(listener);
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/engines/virtual/index.ts
|
|
792
|
+
function createVirtual(options) {
|
|
793
|
+
let itemCount = options.itemCount;
|
|
794
|
+
let itemHeight = options.itemHeight;
|
|
795
|
+
const overscan = options.overscan ?? 5;
|
|
796
|
+
let cache = null;
|
|
797
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
798
|
+
const ensureCache = () => {
|
|
799
|
+
if (cache) return cache;
|
|
800
|
+
cache = createMeasureCache({ itemCount, estimate: itemHeight });
|
|
801
|
+
cache.subscribe((event) => {
|
|
802
|
+
for (const l of listeners) l(event);
|
|
803
|
+
});
|
|
804
|
+
return cache;
|
|
805
|
+
};
|
|
806
|
+
const isFastPath = () => typeof itemHeight === "number" && (cache === null || cache.measuredCount === 0);
|
|
807
|
+
const getEstimate = (index) => typeof itemHeight === "number" ? itemHeight : itemHeight(index);
|
|
808
|
+
const getItemSize = (index) => {
|
|
809
|
+
if (cache) return cache.getItemSize(index);
|
|
810
|
+
return getEstimate(index);
|
|
811
|
+
};
|
|
812
|
+
const getTotalHeight = () => {
|
|
813
|
+
if (isFastPath()) return itemCount * itemHeight;
|
|
814
|
+
return ensureCache().getTotalHeight();
|
|
815
|
+
};
|
|
816
|
+
const getItemOffset = (index) => {
|
|
817
|
+
if (index <= 0) return 0;
|
|
818
|
+
if (index >= itemCount) return getTotalHeight();
|
|
819
|
+
if (isFastPath()) return index * itemHeight;
|
|
820
|
+
return ensureCache().getItemOffset(index);
|
|
821
|
+
};
|
|
822
|
+
const getItemAtOffset = (offset) => {
|
|
823
|
+
if (itemCount === 0) return 0;
|
|
824
|
+
if (isFastPath()) {
|
|
825
|
+
const h = itemHeight;
|
|
826
|
+
return Math.min(Math.floor(offset / h), itemCount - 1);
|
|
827
|
+
}
|
|
828
|
+
return ensureCache().getItemAtOffset(offset);
|
|
829
|
+
};
|
|
830
|
+
const getRange = (scrollTop, viewportHeight) => {
|
|
831
|
+
if (itemCount === 0) {
|
|
832
|
+
return { startIndex: 0, endIndex: 0, offsetTop: 0, totalHeight: 0, visibleItems: [] };
|
|
833
|
+
}
|
|
834
|
+
const total = getTotalHeight();
|
|
835
|
+
const rawStart = getItemAtOffset(scrollTop);
|
|
836
|
+
const startIndex = Math.max(0, rawStart - overscan);
|
|
837
|
+
let endIndex = rawStart;
|
|
838
|
+
let accHeight = getItemOffset(rawStart) + getItemSize(rawStart);
|
|
839
|
+
const bottomEdge = scrollTop + viewportHeight;
|
|
840
|
+
while (endIndex < itemCount - 1 && accHeight < bottomEdge) {
|
|
841
|
+
endIndex++;
|
|
842
|
+
accHeight += getItemSize(endIndex);
|
|
843
|
+
}
|
|
844
|
+
endIndex = Math.min(itemCount - 1, endIndex + overscan);
|
|
845
|
+
const visibleItems = [];
|
|
846
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
847
|
+
visibleItems.push({
|
|
848
|
+
index: i,
|
|
849
|
+
offsetTop: getItemOffset(i),
|
|
850
|
+
height: getItemSize(i)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
startIndex,
|
|
855
|
+
endIndex,
|
|
856
|
+
offsetTop: getItemOffset(startIndex),
|
|
857
|
+
totalHeight: total,
|
|
858
|
+
visibleItems
|
|
859
|
+
};
|
|
860
|
+
};
|
|
861
|
+
const scrollToIndex = (index, viewportHeight, align = "start") => {
|
|
862
|
+
const clamped = Math.max(0, Math.min(index, itemCount - 1));
|
|
863
|
+
const offset = getItemOffset(clamped);
|
|
864
|
+
const h = getItemSize(clamped);
|
|
865
|
+
switch (align) {
|
|
866
|
+
case "center":
|
|
867
|
+
return Math.max(0, offset - viewportHeight / 2 + h / 2);
|
|
868
|
+
case "end":
|
|
869
|
+
return Math.max(0, offset - viewportHeight + h);
|
|
870
|
+
default:
|
|
871
|
+
return offset;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const emit = (event) => {
|
|
875
|
+
for (const l of listeners) l(event);
|
|
876
|
+
};
|
|
877
|
+
return {
|
|
878
|
+
get itemCount() {
|
|
879
|
+
return itemCount;
|
|
880
|
+
},
|
|
881
|
+
get totalHeight() {
|
|
882
|
+
return getTotalHeight();
|
|
883
|
+
},
|
|
884
|
+
get measuredCount() {
|
|
885
|
+
return cache?.measuredCount ?? 0;
|
|
886
|
+
},
|
|
887
|
+
setItemCount(count) {
|
|
888
|
+
if (count === itemCount) return;
|
|
889
|
+
itemCount = count;
|
|
890
|
+
if (cache) {
|
|
891
|
+
cache.setItemCount(count);
|
|
892
|
+
} else {
|
|
893
|
+
emit("resize");
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
setItemHeight(height) {
|
|
897
|
+
itemHeight = height;
|
|
898
|
+
if (cache) {
|
|
899
|
+
cache.setEstimate(height);
|
|
900
|
+
} else {
|
|
901
|
+
emit("reset");
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
setMeasuredHeight(index, height) {
|
|
905
|
+
return ensureCache().setMeasuredHeight(index, height);
|
|
906
|
+
},
|
|
907
|
+
clearMeasuredHeight(index) {
|
|
908
|
+
if (!cache) return false;
|
|
909
|
+
return cache.clearMeasured(index);
|
|
910
|
+
},
|
|
911
|
+
resetMeasurements() {
|
|
912
|
+
if (cache) cache.reset();
|
|
913
|
+
},
|
|
914
|
+
hasMeasured(index) {
|
|
915
|
+
return cache ? cache.hasMeasured(index) : false;
|
|
916
|
+
},
|
|
917
|
+
getEstimatedSize: getEstimate,
|
|
918
|
+
getItemSize,
|
|
919
|
+
getRange,
|
|
920
|
+
getItemOffset,
|
|
921
|
+
getItemAtOffset,
|
|
922
|
+
scrollToIndex,
|
|
923
|
+
subscribe(listener) {
|
|
924
|
+
listeners.add(listener);
|
|
925
|
+
return () => {
|
|
926
|
+
listeners.delete(listener);
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/typed.ts
|
|
933
|
+
function createTypedCore(core) {
|
|
934
|
+
return {
|
|
935
|
+
get: core.get.bind(core),
|
|
936
|
+
set: core.set.bind(core),
|
|
937
|
+
subscribe: core.subscribe.bind(core),
|
|
938
|
+
batch: core.batch.bind(core),
|
|
939
|
+
transaction: core.transaction.bind(core),
|
|
940
|
+
computed: core.computed.bind(core),
|
|
941
|
+
raw: core
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/engines/namespaces.ts
|
|
946
|
+
var reserved = /* @__PURE__ */ new Map();
|
|
947
|
+
function reservePrefix(engine, prefix) {
|
|
948
|
+
if (!prefix.startsWith("$") || !prefix.endsWith(".")) {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`[skygraph] engine "${engine}" tried to reserve invalid prefix "${prefix}". Engine prefixes must start with "$" and end with ".".`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const existing = reserved.get(prefix);
|
|
954
|
+
if (existing !== void 0 && existing !== engine) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`[skygraph] prefix "${prefix}" is already owned by engine "${existing}", cannot reserve it for "${engine}". Pick a different prefix or update the registry.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
reserved.set(prefix, engine);
|
|
960
|
+
return prefix;
|
|
961
|
+
}
|
|
962
|
+
function listReservedPrefixes() {
|
|
963
|
+
return new Map(reserved);
|
|
964
|
+
}
|
|
965
|
+
var FORM_META_PREFIX = reservePrefix("form", "$meta.");
|
|
966
|
+
var TABLE_PREFIX = reservePrefix("table", "$table.");
|
|
967
|
+
var TREE_PREFIX = reservePrefix("tree", "$tree.");
|
|
968
|
+
var GRAPH_PREFIX = reservePrefix("graph", "$graph.");
|
|
969
|
+
var CALENDAR_PREFIX = reservePrefix("calendar", "$calendar.");
|
|
970
|
+
|
|
971
|
+
// src/engines/form/FormEngine.ts
|
|
972
|
+
function metaPath(name, field) {
|
|
973
|
+
return `${FORM_META_PREFIX}${name}.${field}`;
|
|
974
|
+
}
|
|
975
|
+
function interpolateMessage(template, vars) {
|
|
976
|
+
return template.replace(/\$\{(\w+)\}/g, (_, key2) => {
|
|
977
|
+
return vars[key2] != null ? String(vars[key2]) : "";
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function compileRule(rule, templates, fieldLabel) {
|
|
981
|
+
if (typeof rule === "function") return rule;
|
|
982
|
+
const obj = rule;
|
|
983
|
+
const vars = { label: fieldLabel ?? "Field" };
|
|
984
|
+
return (value, getFieldValue) => {
|
|
985
|
+
if (obj.required) {
|
|
986
|
+
if (value === void 0 || value === null || value === "") {
|
|
987
|
+
const tpl = templates?.required ?? obj.message;
|
|
988
|
+
return tpl ? interpolateMessage(tpl, vars) : obj.message ?? "This field is required";
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
992
|
+
if (obj.type) {
|
|
993
|
+
switch (obj.type) {
|
|
994
|
+
case "string":
|
|
995
|
+
if (typeof value !== "string") return obj.message ?? "Must be a string";
|
|
996
|
+
break;
|
|
997
|
+
case "number":
|
|
998
|
+
if (typeof value !== "number" || isNaN(value)) return obj.message ?? "Must be a number";
|
|
999
|
+
break;
|
|
1000
|
+
case "email":
|
|
1001
|
+
if (typeof value !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
1002
|
+
return obj.message ?? "Invalid email";
|
|
1003
|
+
break;
|
|
1004
|
+
case "url":
|
|
1005
|
+
try {
|
|
1006
|
+
new URL(value);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return obj.message ?? "Invalid URL";
|
|
1009
|
+
}
|
|
1010
|
+
break;
|
|
1011
|
+
case "boolean":
|
|
1012
|
+
if (typeof value !== "boolean") return obj.message ?? "Must be a boolean";
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (obj.min !== void 0) {
|
|
1017
|
+
vars.min = obj.min;
|
|
1018
|
+
if (typeof value === "string" && value.length < obj.min) {
|
|
1019
|
+
const tpl = templates?.min ?? obj.message;
|
|
1020
|
+
return tpl ? interpolateMessage(tpl, vars) : obj.message ?? `Minimum ${obj.min} characters`;
|
|
1021
|
+
}
|
|
1022
|
+
if (typeof value === "number" && value < obj.min)
|
|
1023
|
+
return obj.message ?? `Minimum value is ${obj.min}`;
|
|
1024
|
+
}
|
|
1025
|
+
if (obj.max !== void 0) {
|
|
1026
|
+
vars.max = obj.max;
|
|
1027
|
+
if (typeof value === "string" && value.length > obj.max) {
|
|
1028
|
+
const tpl = templates?.max ?? obj.message;
|
|
1029
|
+
return tpl ? interpolateMessage(tpl, vars) : obj.message ?? `Maximum ${obj.max} characters`;
|
|
1030
|
+
}
|
|
1031
|
+
if (typeof value === "number" && value > obj.max)
|
|
1032
|
+
return obj.message ?? `Maximum value is ${obj.max}`;
|
|
1033
|
+
}
|
|
1034
|
+
if (obj.pattern) {
|
|
1035
|
+
if (typeof value === "string" && !obj.pattern.test(value))
|
|
1036
|
+
return obj.message ?? "Invalid format";
|
|
1037
|
+
}
|
|
1038
|
+
if (obj.validator) {
|
|
1039
|
+
return obj.validator(value, getFieldValue);
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function computeStatus(meta) {
|
|
1045
|
+
if (meta.validating) return "validating";
|
|
1046
|
+
if (meta.errors.length > 0) return "error";
|
|
1047
|
+
if (meta.warnings.length > 0) return "warning";
|
|
1048
|
+
return void 0;
|
|
1049
|
+
}
|
|
1050
|
+
function createForm(core, options) {
|
|
1051
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1052
|
+
const depMap = /* @__PURE__ */ new Map();
|
|
1053
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
1054
|
+
const preservedValues = /* @__PURE__ */ new Map();
|
|
1055
|
+
let isSubmitting = false;
|
|
1056
|
+
if (options?.defaultValues) {
|
|
1057
|
+
for (const [key2, value] of Object.entries(options.defaultValues)) {
|
|
1058
|
+
core.set(key2, value);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function setMeta(name, meta) {
|
|
1062
|
+
for (const [key2, value] of Object.entries(meta)) {
|
|
1063
|
+
core.set(metaPath(name, key2), value);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function getMeta(name) {
|
|
1067
|
+
const errors = core.get(metaPath(name, "errors")) ?? [];
|
|
1068
|
+
const warnings = core.get(metaPath(name, "warnings")) ?? [];
|
|
1069
|
+
const validating = core.get(metaPath(name, "validating")) ?? false;
|
|
1070
|
+
const touched = core.get(metaPath(name, "touched")) ?? false;
|
|
1071
|
+
const dirty = core.get(metaPath(name, "dirty")) ?? false;
|
|
1072
|
+
return {
|
|
1073
|
+
touched,
|
|
1074
|
+
dirty,
|
|
1075
|
+
errors,
|
|
1076
|
+
warnings,
|
|
1077
|
+
validating,
|
|
1078
|
+
status: computeStatus({ errors, warnings, validating }),
|
|
1079
|
+
error: errors[0] ?? null
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function initMeta(name) {
|
|
1083
|
+
setMeta(name, {
|
|
1084
|
+
touched: false,
|
|
1085
|
+
dirty: false,
|
|
1086
|
+
errors: [],
|
|
1087
|
+
warnings: [],
|
|
1088
|
+
validating: false
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
const getFieldValue = (fieldName) => core.get(fieldName);
|
|
1092
|
+
function fireValuesChange(changedName) {
|
|
1093
|
+
if (!options?.onValuesChange) return;
|
|
1094
|
+
const changed = { [changedName]: core.get(changedName) };
|
|
1095
|
+
const all = {};
|
|
1096
|
+
for (const n of fields.keys()) all[n] = core.get(n);
|
|
1097
|
+
options.onValuesChange(changed, all);
|
|
1098
|
+
}
|
|
1099
|
+
function fireFieldsChange(changedName) {
|
|
1100
|
+
if (!options?.onFieldsChange) return;
|
|
1101
|
+
const meta = getMeta(changedName);
|
|
1102
|
+
options.onFieldsChange([{
|
|
1103
|
+
name: changedName,
|
|
1104
|
+
value: core.get(changedName),
|
|
1105
|
+
errors: meta.errors
|
|
1106
|
+
}]);
|
|
1107
|
+
}
|
|
1108
|
+
async function runValidation(name) {
|
|
1109
|
+
const record = fields.get(name);
|
|
1110
|
+
if (!record) return [];
|
|
1111
|
+
const value = core.get(name);
|
|
1112
|
+
const validateFirst = record.options.validateFirst ?? options?.validateFirst ?? false;
|
|
1113
|
+
record.abortController?.abort();
|
|
1114
|
+
const controller = new AbortController();
|
|
1115
|
+
record.abortController = controller;
|
|
1116
|
+
const errors = [];
|
|
1117
|
+
for (const rule of record.rules) {
|
|
1118
|
+
if (controller.signal.aborted) return [];
|
|
1119
|
+
const result = rule.fn(value, getFieldValue);
|
|
1120
|
+
if (result instanceof Promise) {
|
|
1121
|
+
setMeta(name, { validating: true });
|
|
1122
|
+
try {
|
|
1123
|
+
const asyncResult = await result;
|
|
1124
|
+
if (controller.signal.aborted) return [];
|
|
1125
|
+
if (asyncResult) {
|
|
1126
|
+
errors.push(asyncResult);
|
|
1127
|
+
if (validateFirst) break;
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
if (controller.signal.aborted) return [];
|
|
1131
|
+
errors.push("Validation failed");
|
|
1132
|
+
if (validateFirst) break;
|
|
1133
|
+
}
|
|
1134
|
+
} else if (result) {
|
|
1135
|
+
errors.push(result);
|
|
1136
|
+
if (validateFirst) break;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const warnings = [];
|
|
1140
|
+
for (const rule of record.warningRules) {
|
|
1141
|
+
if (controller.signal.aborted) return [];
|
|
1142
|
+
const result = rule.fn(value, getFieldValue);
|
|
1143
|
+
if (result instanceof Promise) {
|
|
1144
|
+
try {
|
|
1145
|
+
const asyncResult = await result;
|
|
1146
|
+
if (controller.signal.aborted) return [];
|
|
1147
|
+
if (asyncResult) warnings.push(asyncResult);
|
|
1148
|
+
} catch {
|
|
1149
|
+
}
|
|
1150
|
+
} else if (result) {
|
|
1151
|
+
warnings.push(result);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!controller.signal.aborted) {
|
|
1155
|
+
setMeta(name, { errors, warnings, validating: false });
|
|
1156
|
+
fireFieldsChange(name);
|
|
1157
|
+
}
|
|
1158
|
+
return errors;
|
|
1159
|
+
}
|
|
1160
|
+
async function validateDependents(name) {
|
|
1161
|
+
for (const [target, deps] of depMap) {
|
|
1162
|
+
if (deps.includes(name)) {
|
|
1163
|
+
await runValidation(target);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const form = {
|
|
1168
|
+
register(name, fieldOptions) {
|
|
1169
|
+
const opts = fieldOptions ?? {};
|
|
1170
|
+
const templates = options?.validateMessages;
|
|
1171
|
+
const label = opts.label;
|
|
1172
|
+
fields.set(name, {
|
|
1173
|
+
rules: opts.rules ? opts.rules.map((r) => ({ original: r, fn: compileRule(r, templates, label) })) : [],
|
|
1174
|
+
warningRules: opts.warningRules ? opts.warningRules.map((r) => ({ original: r, fn: compileRule(r, templates, label) })) : [],
|
|
1175
|
+
options: opts,
|
|
1176
|
+
abortController: null
|
|
1177
|
+
});
|
|
1178
|
+
if (preservedValues.has(name)) {
|
|
1179
|
+
core.set(name, preservedValues.get(name));
|
|
1180
|
+
preservedValues.delete(name);
|
|
1181
|
+
} else if (opts.defaultValue !== void 0 && core.get(name) === void 0) {
|
|
1182
|
+
core.set(name, opts.defaultValue);
|
|
1183
|
+
}
|
|
1184
|
+
if (opts.dependencies) {
|
|
1185
|
+
depMap.set(name, [...opts.dependencies]);
|
|
1186
|
+
}
|
|
1187
|
+
core.batch(() => {
|
|
1188
|
+
initMeta(name);
|
|
1189
|
+
});
|
|
1190
|
+
},
|
|
1191
|
+
unregister(name) {
|
|
1192
|
+
const record = fields.get(name);
|
|
1193
|
+
if (!record) return;
|
|
1194
|
+
record.abortController?.abort();
|
|
1195
|
+
const timer = debounceTimers.get(name);
|
|
1196
|
+
if (timer) {
|
|
1197
|
+
clearTimeout(timer);
|
|
1198
|
+
debounceTimers.delete(name);
|
|
1199
|
+
}
|
|
1200
|
+
if (record.options.preserve) {
|
|
1201
|
+
preservedValues.set(name, core.get(name));
|
|
1202
|
+
}
|
|
1203
|
+
fields.delete(name);
|
|
1204
|
+
depMap.delete(name);
|
|
1205
|
+
},
|
|
1206
|
+
setValue(name, value) {
|
|
1207
|
+
core.transaction(() => {
|
|
1208
|
+
core.set(name, value);
|
|
1209
|
+
setMeta(name, { dirty: true, touched: true });
|
|
1210
|
+
});
|
|
1211
|
+
fireValuesChange(name);
|
|
1212
|
+
const mode = fields.get(name)?.options.validateOn ?? options?.validateOn ?? "change";
|
|
1213
|
+
if (mode === "change") {
|
|
1214
|
+
const debounceMs = options?.asyncDebounceMs;
|
|
1215
|
+
if (debounceMs && debounceMs > 0) {
|
|
1216
|
+
const existing = debounceTimers.get(name);
|
|
1217
|
+
if (existing) clearTimeout(existing);
|
|
1218
|
+
debounceTimers.set(
|
|
1219
|
+
name,
|
|
1220
|
+
setTimeout(() => {
|
|
1221
|
+
debounceTimers.delete(name);
|
|
1222
|
+
runValidation(name).then(() => validateDependents(name));
|
|
1223
|
+
}, debounceMs)
|
|
1224
|
+
);
|
|
1225
|
+
} else {
|
|
1226
|
+
runValidation(name).then(() => validateDependents(name));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
getValue(name) {
|
|
1231
|
+
return core.get(name);
|
|
1232
|
+
},
|
|
1233
|
+
onFieldBlur(name) {
|
|
1234
|
+
setMeta(name, { touched: true });
|
|
1235
|
+
const mode = fields.get(name)?.options.validateOn ?? options?.validateOn ?? "change";
|
|
1236
|
+
if (mode === "blur") {
|
|
1237
|
+
runValidation(name).then(() => validateDependents(name));
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
setFieldsValue(values) {
|
|
1241
|
+
core.batch(() => {
|
|
1242
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1243
|
+
core.set(name, value);
|
|
1244
|
+
if (fields.has(name)) {
|
|
1245
|
+
setMeta(name, { dirty: true, touched: true });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
for (const name of Object.keys(values)) {
|
|
1250
|
+
fireValuesChange(name);
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
getFieldsValue() {
|
|
1254
|
+
const values = {};
|
|
1255
|
+
for (const name of fields.keys()) {
|
|
1256
|
+
values[name] = core.get(name);
|
|
1257
|
+
}
|
|
1258
|
+
return values;
|
|
1259
|
+
},
|
|
1260
|
+
getFieldState(name) {
|
|
1261
|
+
return getMeta(name);
|
|
1262
|
+
},
|
|
1263
|
+
getFormState() {
|
|
1264
|
+
let isDirty = false;
|
|
1265
|
+
let isValid = true;
|
|
1266
|
+
let isValidating = false;
|
|
1267
|
+
for (const name of fields.keys()) {
|
|
1268
|
+
const meta = getMeta(name);
|
|
1269
|
+
if (meta.dirty) isDirty = true;
|
|
1270
|
+
if (meta.errors.length > 0) isValid = false;
|
|
1271
|
+
if (meta.validating) isValidating = true;
|
|
1272
|
+
}
|
|
1273
|
+
return { isDirty, isValid, isSubmitting, isValidating };
|
|
1274
|
+
},
|
|
1275
|
+
addRule(name, rule) {
|
|
1276
|
+
const record = fields.get(name);
|
|
1277
|
+
if (!record) return;
|
|
1278
|
+
record.rules.push({
|
|
1279
|
+
original: rule,
|
|
1280
|
+
fn: compileRule(rule, options?.validateMessages, record.options.label)
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1283
|
+
removeRule(name, rule) {
|
|
1284
|
+
const record = fields.get(name);
|
|
1285
|
+
if (!record) return;
|
|
1286
|
+
const idx = record.rules.findIndex((r) => r.original === rule);
|
|
1287
|
+
if (idx !== -1) record.rules.splice(idx, 1);
|
|
1288
|
+
},
|
|
1289
|
+
depends(target, deps) {
|
|
1290
|
+
depMap.set(target, [...deps]);
|
|
1291
|
+
},
|
|
1292
|
+
async validate(name) {
|
|
1293
|
+
const errors = {};
|
|
1294
|
+
const warnings = {};
|
|
1295
|
+
if (name) {
|
|
1296
|
+
const errs = await runValidation(name);
|
|
1297
|
+
if (errs.length > 0) errors[name] = errs;
|
|
1298
|
+
const meta = getMeta(name);
|
|
1299
|
+
if (meta.warnings.length > 0) warnings[name] = meta.warnings;
|
|
1300
|
+
} else {
|
|
1301
|
+
const promises = [];
|
|
1302
|
+
for (const fieldName of fields.keys()) {
|
|
1303
|
+
promises.push(
|
|
1304
|
+
runValidation(fieldName).then((errs) => {
|
|
1305
|
+
if (errs.length > 0) errors[fieldName] = errs;
|
|
1306
|
+
const meta = getMeta(fieldName);
|
|
1307
|
+
if (meta.warnings.length > 0) warnings[fieldName] = meta.warnings;
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
await Promise.all(promises);
|
|
1312
|
+
}
|
|
1313
|
+
return { valid: Object.keys(errors).length === 0, errors, warnings };
|
|
1314
|
+
},
|
|
1315
|
+
async submit(handler) {
|
|
1316
|
+
isSubmitting = true;
|
|
1317
|
+
core.batch(() => {
|
|
1318
|
+
for (const name of fields.keys()) {
|
|
1319
|
+
setMeta(name, { touched: true });
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
const result = await form.validate();
|
|
1323
|
+
if (!result.valid) {
|
|
1324
|
+
isSubmitting = false;
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const values = {};
|
|
1328
|
+
for (const name of fields.keys()) {
|
|
1329
|
+
values[name] = core.get(name);
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
await handler(values);
|
|
1333
|
+
} finally {
|
|
1334
|
+
isSubmitting = false;
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
reset(values) {
|
|
1338
|
+
core.batch(() => {
|
|
1339
|
+
for (const [name, record] of fields) {
|
|
1340
|
+
record.abortController?.abort();
|
|
1341
|
+
record.abortController = null;
|
|
1342
|
+
const resetValue = values?.[name] ?? options?.defaultValues?.[name] ?? record.options.defaultValue ?? void 0;
|
|
1343
|
+
core.set(name, resetValue);
|
|
1344
|
+
initMeta(name);
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
preservedValues.clear();
|
|
1348
|
+
},
|
|
1349
|
+
getFieldNames() {
|
|
1350
|
+
return [...fields.keys()];
|
|
1351
|
+
},
|
|
1352
|
+
// Manual error/warning control
|
|
1353
|
+
setFieldError(name, errs) {
|
|
1354
|
+
const arr = typeof errs === "string" ? [errs] : errs;
|
|
1355
|
+
setMeta(name, { errors: arr });
|
|
1356
|
+
fireFieldsChange(name);
|
|
1357
|
+
},
|
|
1358
|
+
setFieldWarning(name, warns) {
|
|
1359
|
+
const arr = typeof warns === "string" ? [warns] : warns;
|
|
1360
|
+
setMeta(name, { warnings: arr });
|
|
1361
|
+
},
|
|
1362
|
+
clearFieldErrors(name) {
|
|
1363
|
+
setMeta(name, { errors: [], warnings: [] });
|
|
1364
|
+
fireFieldsChange(name);
|
|
1365
|
+
},
|
|
1366
|
+
getFieldErrors(name) {
|
|
1367
|
+
return getMeta(name).errors;
|
|
1368
|
+
},
|
|
1369
|
+
getFieldWarnings(name) {
|
|
1370
|
+
return getMeta(name).warnings;
|
|
1371
|
+
},
|
|
1372
|
+
isFieldTouched(name) {
|
|
1373
|
+
return getMeta(name).touched;
|
|
1374
|
+
},
|
|
1375
|
+
isFieldValidating(name) {
|
|
1376
|
+
return getMeta(name).validating;
|
|
1377
|
+
},
|
|
1378
|
+
// List operations
|
|
1379
|
+
getListValue(name) {
|
|
1380
|
+
const val = core.get(name);
|
|
1381
|
+
return Array.isArray(val) ? val : [];
|
|
1382
|
+
},
|
|
1383
|
+
listAdd(name, defaultValue, insertIndex) {
|
|
1384
|
+
const current = form.getListValue(name);
|
|
1385
|
+
if (insertIndex !== void 0 && insertIndex >= 0 && insertIndex <= current.length) {
|
|
1386
|
+
const next = [...current];
|
|
1387
|
+
next.splice(insertIndex, 0, defaultValue ?? null);
|
|
1388
|
+
core.set(name, next);
|
|
1389
|
+
} else {
|
|
1390
|
+
core.set(name, [...current, defaultValue ?? null]);
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1393
|
+
listRemove(name, index) {
|
|
1394
|
+
const current = form.getListValue(name);
|
|
1395
|
+
if (Array.isArray(index)) {
|
|
1396
|
+
const indices = new Set(index);
|
|
1397
|
+
core.set(name, current.filter((_, i) => !indices.has(i)));
|
|
1398
|
+
} else {
|
|
1399
|
+
core.set(name, current.filter((_, i) => i !== index));
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
listMove(name, from, to) {
|
|
1403
|
+
const current = [...form.getListValue(name)];
|
|
1404
|
+
if (from < 0 || from >= current.length || to < 0 || to >= current.length) return;
|
|
1405
|
+
const [item] = current.splice(from, 1);
|
|
1406
|
+
current.splice(to, 0, item);
|
|
1407
|
+
core.set(name, current);
|
|
1408
|
+
},
|
|
1409
|
+
listReplace(name, values) {
|
|
1410
|
+
core.set(name, [...values]);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
return form;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/engines/graph/history.ts
|
|
1417
|
+
var HISTORY_LIMIT = 100;
|
|
1418
|
+
function createHistory2(limit = HISTORY_LIMIT) {
|
|
1419
|
+
const undoStack = [];
|
|
1420
|
+
const redoStack = [];
|
|
1421
|
+
function trim(stack) {
|
|
1422
|
+
while (stack.length > limit) stack.shift();
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
push(entry) {
|
|
1426
|
+
undoStack.push(entry);
|
|
1427
|
+
trim(undoStack);
|
|
1428
|
+
},
|
|
1429
|
+
popUndo() {
|
|
1430
|
+
return undoStack.pop();
|
|
1431
|
+
},
|
|
1432
|
+
pushRedo(entry) {
|
|
1433
|
+
redoStack.push(entry);
|
|
1434
|
+
trim(redoStack);
|
|
1435
|
+
},
|
|
1436
|
+
popRedo() {
|
|
1437
|
+
return redoStack.pop();
|
|
1438
|
+
},
|
|
1439
|
+
clearRedo() {
|
|
1440
|
+
redoStack.length = 0;
|
|
1441
|
+
},
|
|
1442
|
+
canUndo() {
|
|
1443
|
+
return undoStack.length > 0;
|
|
1444
|
+
},
|
|
1445
|
+
canRedo() {
|
|
1446
|
+
return redoStack.length > 0;
|
|
1447
|
+
},
|
|
1448
|
+
clear() {
|
|
1449
|
+
undoStack.length = 0;
|
|
1450
|
+
redoStack.length = 0;
|
|
1451
|
+
},
|
|
1452
|
+
relabelTop(label) {
|
|
1453
|
+
const top = undoStack[undoStack.length - 1];
|
|
1454
|
+
if (!top) return;
|
|
1455
|
+
undoStack[undoStack.length - 1] = { snapshot: top.snapshot, label };
|
|
1456
|
+
},
|
|
1457
|
+
topLabel() {
|
|
1458
|
+
return undoStack[undoStack.length - 1]?.label;
|
|
1459
|
+
},
|
|
1460
|
+
size() {
|
|
1461
|
+
return { undo: undoStack.length, redo: redoStack.length };
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/engines/graph/GraphEngine.ts
|
|
1467
|
+
var engineCounter = 0;
|
|
1468
|
+
var DEFAULT_OUTLINE = { kind: "rect", w: 100, h: 60 };
|
|
1469
|
+
var DEFAULT_TRANSFORM = { x: 0, y: 0 };
|
|
1470
|
+
var DEFAULT_ANCHOR_POLICY = { kind: "perEdge", k: 1 };
|
|
1471
|
+
function createGraph(core, options) {
|
|
1472
|
+
const engineId = `g${engineCounter++}${options?.name ? `-${options.name}` : ""}`;
|
|
1473
|
+
const prefix = GRAPH_PREFIX;
|
|
1474
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1475
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1476
|
+
const edgesByNode = /* @__PURE__ */ new Map();
|
|
1477
|
+
let nodeCounter = 0;
|
|
1478
|
+
let edgeCounter = 0;
|
|
1479
|
+
const localSubscribers = /* @__PURE__ */ new Set();
|
|
1480
|
+
function publishSnapshot() {
|
|
1481
|
+
core.set(`${prefix}snapshot.${engineId}`, {
|
|
1482
|
+
nodes: Array.from(nodes.values()),
|
|
1483
|
+
edges: Array.from(edges.values())
|
|
1484
|
+
});
|
|
1485
|
+
for (const cb of localSubscribers) cb();
|
|
1486
|
+
}
|
|
1487
|
+
function subscribeLocal(cb) {
|
|
1488
|
+
localSubscribers.add(cb);
|
|
1489
|
+
return () => {
|
|
1490
|
+
localSubscribers.delete(cb);
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
publishSnapshot();
|
|
1494
|
+
const history = createHistory2(HISTORY_LIMIT);
|
|
1495
|
+
let transactionDepth = 0;
|
|
1496
|
+
let pendingEntry = null;
|
|
1497
|
+
let isRestoring = false;
|
|
1498
|
+
function _captureState() {
|
|
1499
|
+
return {
|
|
1500
|
+
nodes: Array.from(nodes.values()),
|
|
1501
|
+
edges: Array.from(edges.values()),
|
|
1502
|
+
nodeCounter,
|
|
1503
|
+
edgeCounter
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function _restoreState(snapshot) {
|
|
1507
|
+
nodes.clear();
|
|
1508
|
+
edges.clear();
|
|
1509
|
+
edgesByNode.clear();
|
|
1510
|
+
for (const n of snapshot.nodes) {
|
|
1511
|
+
nodes.set(n.id, n);
|
|
1512
|
+
edgesByNode.set(n.id, /* @__PURE__ */ new Set());
|
|
1513
|
+
}
|
|
1514
|
+
for (const e of snapshot.edges) {
|
|
1515
|
+
edges.set(e.id, e);
|
|
1516
|
+
ensureBucket(endpointNode(e.from)).add(e.id);
|
|
1517
|
+
ensureBucket(endpointNode(e.to)).add(e.id);
|
|
1518
|
+
}
|
|
1519
|
+
nodeCounter = snapshot.nodeCounter;
|
|
1520
|
+
edgeCounter = snapshot.edgeCounter;
|
|
1521
|
+
}
|
|
1522
|
+
function _withHistory(fn, label) {
|
|
1523
|
+
if (isRestoring) return fn();
|
|
1524
|
+
const wasOuter = transactionDepth === 0;
|
|
1525
|
+
if (wasOuter) {
|
|
1526
|
+
pendingEntry = { snapshot: _captureState(), label };
|
|
1527
|
+
} else if (label !== void 0 && pendingEntry && pendingEntry.label === void 0) {
|
|
1528
|
+
pendingEntry = { snapshot: pendingEntry.snapshot, label };
|
|
1529
|
+
}
|
|
1530
|
+
transactionDepth++;
|
|
1531
|
+
try {
|
|
1532
|
+
return fn();
|
|
1533
|
+
} finally {
|
|
1534
|
+
transactionDepth--;
|
|
1535
|
+
if (transactionDepth === 0 && wasOuter) {
|
|
1536
|
+
if (pendingEntry !== null) {
|
|
1537
|
+
history.push(pendingEntry);
|
|
1538
|
+
history.clearRedo();
|
|
1539
|
+
}
|
|
1540
|
+
pendingEntry = null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function newNodeId() {
|
|
1545
|
+
return `${engineId}-n${nodeCounter++}`;
|
|
1546
|
+
}
|
|
1547
|
+
function newEdgeId() {
|
|
1548
|
+
return `${engineId}-e${edgeCounter++}`;
|
|
1549
|
+
}
|
|
1550
|
+
function ensureBucket(nodeId) {
|
|
1551
|
+
let bucket = edgesByNode.get(nodeId);
|
|
1552
|
+
if (!bucket) {
|
|
1553
|
+
bucket = /* @__PURE__ */ new Set();
|
|
1554
|
+
edgesByNode.set(nodeId, bucket);
|
|
1555
|
+
}
|
|
1556
|
+
return bucket;
|
|
1557
|
+
}
|
|
1558
|
+
function endpointNode(ep) {
|
|
1559
|
+
return ep.node;
|
|
1560
|
+
}
|
|
1561
|
+
function bumpRevision(node) {
|
|
1562
|
+
return { ...node, geomRevision: node.geomRevision + 1 };
|
|
1563
|
+
}
|
|
1564
|
+
function _addNodeImpl(init = {}) {
|
|
1565
|
+
const id = init.id ?? newNodeId();
|
|
1566
|
+
if (nodes.has(id)) {
|
|
1567
|
+
throw new Error(`[skygraph/graph] node id "${id}" already exists`);
|
|
1568
|
+
}
|
|
1569
|
+
const node = {
|
|
1570
|
+
id,
|
|
1571
|
+
parentId: init.parentId ?? null,
|
|
1572
|
+
transform: init.transform ?? { ...DEFAULT_TRANSFORM },
|
|
1573
|
+
outline: init.outline ?? DEFAULT_OUTLINE,
|
|
1574
|
+
anchorPolicy: init.anchorPolicy ?? DEFAULT_ANCHOR_POLICY,
|
|
1575
|
+
data: init.data,
|
|
1576
|
+
geomRevision: 0
|
|
1577
|
+
};
|
|
1578
|
+
nodes.set(id, node);
|
|
1579
|
+
edgesByNode.set(id, /* @__PURE__ */ new Set());
|
|
1580
|
+
publishSnapshot();
|
|
1581
|
+
return id;
|
|
1582
|
+
}
|
|
1583
|
+
function addNode(init = {}) {
|
|
1584
|
+
return _withHistory(() => _addNodeImpl(init));
|
|
1585
|
+
}
|
|
1586
|
+
function _removeNodeImpl(id) {
|
|
1587
|
+
if (!nodes.has(id)) return;
|
|
1588
|
+
const incident = edgesByNode.get(id);
|
|
1589
|
+
if (incident) {
|
|
1590
|
+
for (const eid of incident) removeEdge(eid);
|
|
1591
|
+
}
|
|
1592
|
+
for (const [otherId, n] of nodes) {
|
|
1593
|
+
if (n.parentId === id) {
|
|
1594
|
+
nodes.set(otherId, { ...n, parentId: null });
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
nodes.delete(id);
|
|
1598
|
+
edgesByNode.delete(id);
|
|
1599
|
+
publishSnapshot();
|
|
1600
|
+
}
|
|
1601
|
+
function removeNode(id) {
|
|
1602
|
+
_withHistory(() => _removeNodeImpl(id));
|
|
1603
|
+
}
|
|
1604
|
+
function _updateNodeImpl(id, patch) {
|
|
1605
|
+
const node = nodes.get(id);
|
|
1606
|
+
if (!node) {
|
|
1607
|
+
throw new Error(`[skygraph/graph] node "${id}" not found`);
|
|
1608
|
+
}
|
|
1609
|
+
const next = {
|
|
1610
|
+
...node,
|
|
1611
|
+
transform: patch.transform ? { ...node.transform, ...patch.transform } : node.transform,
|
|
1612
|
+
outline: patch.outline ?? node.outline,
|
|
1613
|
+
anchorPolicy: patch.anchorPolicy ?? node.anchorPolicy,
|
|
1614
|
+
data: patch.data === void 0 ? node.data : patch.data
|
|
1615
|
+
};
|
|
1616
|
+
const geomChanged = patch.transform !== void 0 || patch.outline !== void 0 || patch.anchorPolicy !== void 0;
|
|
1617
|
+
nodes.set(id, geomChanged ? bumpRevision(next) : next);
|
|
1618
|
+
publishSnapshot();
|
|
1619
|
+
}
|
|
1620
|
+
function updateNode(id, patch) {
|
|
1621
|
+
_withHistory(() => _updateNodeImpl(id, patch));
|
|
1622
|
+
}
|
|
1623
|
+
function moveNode(id, x, y) {
|
|
1624
|
+
_withHistory(() => _updateNodeImpl(id, { transform: { x, y } }));
|
|
1625
|
+
}
|
|
1626
|
+
function _setParentImpl(id, parentId) {
|
|
1627
|
+
const node = nodes.get(id);
|
|
1628
|
+
if (!node) {
|
|
1629
|
+
throw new Error(`[skygraph/graph] node "${id}" not found`);
|
|
1630
|
+
}
|
|
1631
|
+
if (parentId !== null && !nodes.has(parentId)) {
|
|
1632
|
+
throw new Error(`[skygraph/graph] parent node "${parentId}" not found`);
|
|
1633
|
+
}
|
|
1634
|
+
if (parentId === id) {
|
|
1635
|
+
throw new Error(`[skygraph/graph] node cannot be its own parent ("${id}")`);
|
|
1636
|
+
}
|
|
1637
|
+
let cursor = parentId;
|
|
1638
|
+
while (cursor !== null) {
|
|
1639
|
+
if (cursor === id) {
|
|
1640
|
+
throw new Error(`[skygraph/graph] setParent would create a cycle ("${id}" \u2192 "${parentId}")`);
|
|
1641
|
+
}
|
|
1642
|
+
cursor = nodes.get(cursor)?.parentId ?? null;
|
|
1643
|
+
}
|
|
1644
|
+
nodes.set(id, { ...node, parentId });
|
|
1645
|
+
publishSnapshot();
|
|
1646
|
+
}
|
|
1647
|
+
function setParent(id, parentId) {
|
|
1648
|
+
_withHistory(() => _setParentImpl(id, parentId));
|
|
1649
|
+
}
|
|
1650
|
+
function _addEdgeImpl(init) {
|
|
1651
|
+
const id = init.id ?? newEdgeId();
|
|
1652
|
+
if (edges.has(id)) {
|
|
1653
|
+
throw new Error(`[skygraph/graph] edge id "${id}" already exists`);
|
|
1654
|
+
}
|
|
1655
|
+
const fromId = endpointNode(init.from);
|
|
1656
|
+
const toId = endpointNode(init.to);
|
|
1657
|
+
if (!nodes.has(fromId)) {
|
|
1658
|
+
throw new Error(`[skygraph/graph] addEdge: from-node "${fromId}" not found`);
|
|
1659
|
+
}
|
|
1660
|
+
if (!nodes.has(toId)) {
|
|
1661
|
+
throw new Error(`[skygraph/graph] addEdge: to-node "${toId}" not found`);
|
|
1662
|
+
}
|
|
1663
|
+
const edge = {
|
|
1664
|
+
id,
|
|
1665
|
+
from: init.from,
|
|
1666
|
+
to: init.to,
|
|
1667
|
+
routing: init.routing ?? "straight",
|
|
1668
|
+
waypoints: init.waypoints,
|
|
1669
|
+
data: init.data
|
|
1670
|
+
};
|
|
1671
|
+
edges.set(id, edge);
|
|
1672
|
+
ensureBucket(fromId).add(id);
|
|
1673
|
+
ensureBucket(toId).add(id);
|
|
1674
|
+
publishSnapshot();
|
|
1675
|
+
return id;
|
|
1676
|
+
}
|
|
1677
|
+
function addEdge(init) {
|
|
1678
|
+
return _withHistory(() => _addEdgeImpl(init));
|
|
1679
|
+
}
|
|
1680
|
+
function _removeEdgeImpl(id) {
|
|
1681
|
+
const edge = edges.get(id);
|
|
1682
|
+
if (!edge) return;
|
|
1683
|
+
edgesByNode.get(endpointNode(edge.from))?.delete(id);
|
|
1684
|
+
edgesByNode.get(endpointNode(edge.to))?.delete(id);
|
|
1685
|
+
edges.delete(id);
|
|
1686
|
+
publishSnapshot();
|
|
1687
|
+
}
|
|
1688
|
+
function removeEdge(id) {
|
|
1689
|
+
_withHistory(() => _removeEdgeImpl(id));
|
|
1690
|
+
}
|
|
1691
|
+
function getNode(id) {
|
|
1692
|
+
return nodes.get(id);
|
|
1693
|
+
}
|
|
1694
|
+
function getEdge(id) {
|
|
1695
|
+
return edges.get(id);
|
|
1696
|
+
}
|
|
1697
|
+
function getState() {
|
|
1698
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
1699
|
+
for (const [nid, set] of edgesByNode) byNode.set(nid, Array.from(set));
|
|
1700
|
+
return {
|
|
1701
|
+
nodes: new Map(nodes),
|
|
1702
|
+
edges: new Map(edges),
|
|
1703
|
+
edgesByNode: byNode
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
function childrenOf(id) {
|
|
1707
|
+
const out = [];
|
|
1708
|
+
for (const n of nodes.values()) {
|
|
1709
|
+
if (n.parentId === id) out.push(n.id);
|
|
1710
|
+
}
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
function edgesOf(id) {
|
|
1714
|
+
return Array.from(edgesByNode.get(id) ?? []);
|
|
1715
|
+
}
|
|
1716
|
+
function rectCornerId(corner) {
|
|
1717
|
+
return corner;
|
|
1718
|
+
}
|
|
1719
|
+
function rectAnchors(w, h, policy) {
|
|
1720
|
+
const corners = [
|
|
1721
|
+
{ id: rectCornerId("nw"), s: 0, point: [0, 0] },
|
|
1722
|
+
{ id: rectCornerId("ne"), s: 0.25, point: [w, 0] },
|
|
1723
|
+
{ id: rectCornerId("se"), s: 0.5, point: [w, h] },
|
|
1724
|
+
{ id: rectCornerId("sw"), s: 0.75, point: [0, h] }
|
|
1725
|
+
];
|
|
1726
|
+
if (policy.kind === "manual") {
|
|
1727
|
+
return policy.anchors.map((a) => ({
|
|
1728
|
+
id: a.id,
|
|
1729
|
+
s: a.s,
|
|
1730
|
+
point: pointOnRectPerimeter(w, h, a.s)
|
|
1731
|
+
}));
|
|
1732
|
+
}
|
|
1733
|
+
const k = policy.kind === "perEdge" ? Math.max(0, Math.floor(policy.k)) : null;
|
|
1734
|
+
const density = policy.kind === "byLength" ? policy.density : null;
|
|
1735
|
+
const out = [...corners];
|
|
1736
|
+
const edgesGeom = [
|
|
1737
|
+
{ start: 0, end: 0.25 },
|
|
1738
|
+
// top
|
|
1739
|
+
{ start: 0.25, end: 0.5 },
|
|
1740
|
+
// right
|
|
1741
|
+
{ start: 0.5, end: 0.75 },
|
|
1742
|
+
// bottom
|
|
1743
|
+
{ start: 0.75, end: 1 }
|
|
1744
|
+
// left
|
|
1745
|
+
];
|
|
1746
|
+
edgesGeom.forEach((edge, edgeIdx) => {
|
|
1747
|
+
let count = 0;
|
|
1748
|
+
if (k !== null) count = k;
|
|
1749
|
+
if (density !== null) {
|
|
1750
|
+
const len = edgeIdx % 2 === 0 ? w : h;
|
|
1751
|
+
count = Math.max(0, Math.floor(len / density) - 1);
|
|
1752
|
+
}
|
|
1753
|
+
for (let i = 1; i <= count; i++) {
|
|
1754
|
+
const t = i / (count + 1);
|
|
1755
|
+
const s = edge.start + (edge.end - edge.start) * t;
|
|
1756
|
+
out.push({
|
|
1757
|
+
id: `edge${edgeIdx}-${i}of${count}`,
|
|
1758
|
+
s,
|
|
1759
|
+
point: pointOnRectPerimeter(w, h, s)
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
out.sort((a, b) => a.s - b.s);
|
|
1764
|
+
return out;
|
|
1765
|
+
}
|
|
1766
|
+
function pointOnRectPerimeter(w, h, s) {
|
|
1767
|
+
const ss = (s % 1 + 1) % 1;
|
|
1768
|
+
if (ss < 0.25) return [w * (ss / 0.25), 0];
|
|
1769
|
+
if (ss < 0.5) return [w, h * ((ss - 0.25) / 0.25)];
|
|
1770
|
+
if (ss < 0.75) return [w * (1 - (ss - 0.5) / 0.25), h];
|
|
1771
|
+
return [0, h * (1 - (ss - 0.75) / 0.25)];
|
|
1772
|
+
}
|
|
1773
|
+
function polygonAnchors(verts, policy) {
|
|
1774
|
+
if (verts.length === 0) return [];
|
|
1775
|
+
const segLengths = [];
|
|
1776
|
+
let total = 0;
|
|
1777
|
+
for (let i = 0; i < verts.length; i++) {
|
|
1778
|
+
const a = verts[i];
|
|
1779
|
+
const b = verts[(i + 1) % verts.length];
|
|
1780
|
+
const dx = b[0] - a[0];
|
|
1781
|
+
const dy = b[1] - a[1];
|
|
1782
|
+
const len = Math.hypot(dx, dy);
|
|
1783
|
+
segLengths.push(len);
|
|
1784
|
+
total += len;
|
|
1785
|
+
}
|
|
1786
|
+
const cumS = [0];
|
|
1787
|
+
for (let i = 0; i < segLengths.length; i++) {
|
|
1788
|
+
cumS.push(cumS[i] + segLengths[i] / total);
|
|
1789
|
+
}
|
|
1790
|
+
function pointAt(s) {
|
|
1791
|
+
const ss = (s % 1 + 1) % 1;
|
|
1792
|
+
let i = 0;
|
|
1793
|
+
while (i < cumS.length - 1 && cumS[i + 1] <= ss) i++;
|
|
1794
|
+
const a = verts[i % verts.length];
|
|
1795
|
+
const b = verts[(i + 1) % verts.length];
|
|
1796
|
+
const segS0 = cumS[i];
|
|
1797
|
+
const segS1 = cumS[i + 1];
|
|
1798
|
+
const t = segS1 === segS0 ? 0 : (ss - segS0) / (segS1 - segS0);
|
|
1799
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
1800
|
+
}
|
|
1801
|
+
if (policy.kind === "manual") {
|
|
1802
|
+
return policy.anchors.map((a) => ({ id: a.id, s: a.s, point: pointAt(a.s) }));
|
|
1803
|
+
}
|
|
1804
|
+
const out = [];
|
|
1805
|
+
for (let i = 0; i < verts.length; i++) {
|
|
1806
|
+
out.push({
|
|
1807
|
+
id: `v${i}`,
|
|
1808
|
+
s: cumS[i],
|
|
1809
|
+
point: [verts[i][0], verts[i][1]]
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
const k = policy.kind === "perEdge" ? Math.max(0, Math.floor(policy.k)) : null;
|
|
1813
|
+
const density = policy.kind === "byLength" ? policy.density : null;
|
|
1814
|
+
for (let i = 0; i < verts.length; i++) {
|
|
1815
|
+
const segLen = segLengths[i];
|
|
1816
|
+
let count = 0;
|
|
1817
|
+
if (k !== null) count = k;
|
|
1818
|
+
if (density !== null) count = Math.max(0, Math.floor(segLen / density) - 1);
|
|
1819
|
+
const s0 = cumS[i];
|
|
1820
|
+
const s1 = cumS[i + 1];
|
|
1821
|
+
for (let j = 1; j <= count; j++) {
|
|
1822
|
+
const t = j / (count + 1);
|
|
1823
|
+
const s = s0 + (s1 - s0) * t;
|
|
1824
|
+
out.push({ id: `v${i}-${j}of${count}`, s, point: pointAt(s) });
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
out.sort((a, b) => a.s - b.s);
|
|
1828
|
+
return out;
|
|
1829
|
+
}
|
|
1830
|
+
function ellipseAnchors(rx, ry, policy) {
|
|
1831
|
+
function pointAt(s) {
|
|
1832
|
+
const angle = (s % 1 + 1) % 1 * Math.PI * 2;
|
|
1833
|
+
return [rx * Math.cos(angle), ry * Math.sin(angle)];
|
|
1834
|
+
}
|
|
1835
|
+
if (policy.kind === "manual") {
|
|
1836
|
+
return policy.anchors.map((a) => ({ id: a.id, s: a.s, point: pointAt(a.s) }));
|
|
1837
|
+
}
|
|
1838
|
+
const k = policy.kind === "perEdge" ? Math.max(1, Math.floor(policy.k)) : null;
|
|
1839
|
+
const totalSamples = k !== null ? Math.max(4, k * 4) : Math.max(4, Math.floor(2 * Math.PI * Math.max(rx, ry) / policy.density));
|
|
1840
|
+
const out = [];
|
|
1841
|
+
for (let i = 0; i < totalSamples; i++) {
|
|
1842
|
+
const s = i / totalSamples;
|
|
1843
|
+
out.push({ id: `el${i}of${totalSamples}`, s, point: pointAt(s) });
|
|
1844
|
+
}
|
|
1845
|
+
return out;
|
|
1846
|
+
}
|
|
1847
|
+
function anchorsOf(id) {
|
|
1848
|
+
const node = nodes.get(id);
|
|
1849
|
+
if (!node) return [];
|
|
1850
|
+
const o = node.outline;
|
|
1851
|
+
if (o.kind === "rect") return rectAnchors(o.w, o.h, node.anchorPolicy);
|
|
1852
|
+
if (o.kind === "polygon") return polygonAnchors(o.verts, node.anchorPolicy);
|
|
1853
|
+
if (o.kind === "ellipse") return ellipseAnchors(o.rx, o.ry, node.anchorPolicy);
|
|
1854
|
+
if (o.kind === "path") return polygonAnchors(o.flatten, node.anchorPolicy);
|
|
1855
|
+
return [];
|
|
1856
|
+
}
|
|
1857
|
+
function localBounds(node) {
|
|
1858
|
+
const o = node.outline;
|
|
1859
|
+
if (o.kind === "rect") return { x0: 0, y0: 0, x1: o.w, y1: o.h };
|
|
1860
|
+
if (o.kind === "ellipse") return { x0: -o.rx, y0: -o.ry, x1: o.rx, y1: o.ry };
|
|
1861
|
+
const verts = o.kind === "polygon" ? o.verts : o.flatten;
|
|
1862
|
+
if (verts.length === 0) return { x0: 0, y0: 0, x1: 0, y1: 0 };
|
|
1863
|
+
let x0 = verts[0][0];
|
|
1864
|
+
let x1 = verts[0][0];
|
|
1865
|
+
let y0 = verts[0][1];
|
|
1866
|
+
let y1 = verts[0][1];
|
|
1867
|
+
for (const [x, y] of verts) {
|
|
1868
|
+
if (x < x0) x0 = x;
|
|
1869
|
+
if (x > x1) x1 = x;
|
|
1870
|
+
if (y < y0) y0 = y;
|
|
1871
|
+
if (y > y1) y1 = y;
|
|
1872
|
+
}
|
|
1873
|
+
return { x0, y0, x1, y1 };
|
|
1874
|
+
}
|
|
1875
|
+
function worldOriginOf(id) {
|
|
1876
|
+
let cursor = id;
|
|
1877
|
+
let x = 0;
|
|
1878
|
+
let y = 0;
|
|
1879
|
+
let scale = 1;
|
|
1880
|
+
while (cursor !== null) {
|
|
1881
|
+
const n = nodes.get(cursor);
|
|
1882
|
+
if (!n) break;
|
|
1883
|
+
const s = n.transform.scale ?? 1;
|
|
1884
|
+
x = n.transform.x + x * s;
|
|
1885
|
+
y = n.transform.y + y * s;
|
|
1886
|
+
scale *= s;
|
|
1887
|
+
cursor = n.parentId;
|
|
1888
|
+
}
|
|
1889
|
+
return { x, y, scale };
|
|
1890
|
+
}
|
|
1891
|
+
function boundsOf(id) {
|
|
1892
|
+
const node = nodes.get(id);
|
|
1893
|
+
if (!node) return { x: 0, y: 0, w: 0, h: 0 };
|
|
1894
|
+
const lb = localBounds(node);
|
|
1895
|
+
const origin = worldOriginOf(id);
|
|
1896
|
+
const w = (lb.x1 - lb.x0) * origin.scale;
|
|
1897
|
+
const h = (lb.y1 - lb.y0) * origin.scale;
|
|
1898
|
+
return {
|
|
1899
|
+
x: origin.x + lb.x0 * origin.scale,
|
|
1900
|
+
y: origin.y + lb.y0 * origin.scale,
|
|
1901
|
+
w,
|
|
1902
|
+
h
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
function getNodeOBB(id) {
|
|
1906
|
+
const node = nodes.get(id);
|
|
1907
|
+
if (!node) return { center: [0, 0], halfWidth: 0, halfHeight: 0, angle: 0 };
|
|
1908
|
+
const lb = localBounds(node);
|
|
1909
|
+
const origin = worldOriginOf(id);
|
|
1910
|
+
const localCx = (lb.x0 + lb.x1) / 2;
|
|
1911
|
+
const localCy = (lb.y0 + lb.y1) / 2;
|
|
1912
|
+
const halfWidth = Math.abs((lb.x1 - lb.x0) / 2 * origin.scale);
|
|
1913
|
+
const halfHeight = Math.abs((lb.y1 - lb.y0) / 2 * origin.scale);
|
|
1914
|
+
let angle = 0;
|
|
1915
|
+
let cursor = id;
|
|
1916
|
+
while (cursor !== null) {
|
|
1917
|
+
const n = nodes.get(cursor);
|
|
1918
|
+
if (!n) break;
|
|
1919
|
+
angle += n.transform.rot ?? 0;
|
|
1920
|
+
cursor = n.parentId;
|
|
1921
|
+
}
|
|
1922
|
+
const center = [
|
|
1923
|
+
origin.x + localCx * origin.scale,
|
|
1924
|
+
origin.y + localCy * origin.scale
|
|
1925
|
+
];
|
|
1926
|
+
return { center, halfWidth, halfHeight, angle };
|
|
1927
|
+
}
|
|
1928
|
+
function clear() {
|
|
1929
|
+
nodes.clear();
|
|
1930
|
+
edges.clear();
|
|
1931
|
+
edgesByNode.clear();
|
|
1932
|
+
nodeCounter = 0;
|
|
1933
|
+
edgeCounter = 0;
|
|
1934
|
+
history.clear();
|
|
1935
|
+
pendingEntry = null;
|
|
1936
|
+
transactionDepth = 0;
|
|
1937
|
+
isRestoring = false;
|
|
1938
|
+
publishSnapshot();
|
|
1939
|
+
}
|
|
1940
|
+
function transaction(fn, label) {
|
|
1941
|
+
_withHistory(fn, label);
|
|
1942
|
+
}
|
|
1943
|
+
function pushHistory(label) {
|
|
1944
|
+
if (transactionDepth > 0) {
|
|
1945
|
+
if (pendingEntry !== null) {
|
|
1946
|
+
history.push({ snapshot: pendingEntry.snapshot, label: pendingEntry.label ?? label });
|
|
1947
|
+
history.clearRedo();
|
|
1948
|
+
}
|
|
1949
|
+
pendingEntry = { snapshot: _captureState(), label: void 0 };
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
history.relabelTop(label);
|
|
1953
|
+
}
|
|
1954
|
+
function undo() {
|
|
1955
|
+
if (!history.canUndo()) return false;
|
|
1956
|
+
const entry = history.popUndo();
|
|
1957
|
+
history.pushRedo({ snapshot: _captureState(), label: entry.label });
|
|
1958
|
+
isRestoring = true;
|
|
1959
|
+
try {
|
|
1960
|
+
_restoreState(entry.snapshot);
|
|
1961
|
+
} finally {
|
|
1962
|
+
isRestoring = false;
|
|
1963
|
+
}
|
|
1964
|
+
publishSnapshot();
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
function redo() {
|
|
1968
|
+
if (!history.canRedo()) return false;
|
|
1969
|
+
const entry = history.popRedo();
|
|
1970
|
+
history.push({ snapshot: _captureState(), label: entry.label });
|
|
1971
|
+
isRestoring = true;
|
|
1972
|
+
try {
|
|
1973
|
+
_restoreState(entry.snapshot);
|
|
1974
|
+
} finally {
|
|
1975
|
+
isRestoring = false;
|
|
1976
|
+
}
|
|
1977
|
+
publishSnapshot();
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
function canUndo() {
|
|
1981
|
+
return history.canUndo();
|
|
1982
|
+
}
|
|
1983
|
+
function canRedo() {
|
|
1984
|
+
return history.canRedo();
|
|
1985
|
+
}
|
|
1986
|
+
function clearHistory() {
|
|
1987
|
+
history.clear();
|
|
1988
|
+
}
|
|
1989
|
+
return {
|
|
1990
|
+
addNode,
|
|
1991
|
+
removeNode,
|
|
1992
|
+
updateNode,
|
|
1993
|
+
moveNode,
|
|
1994
|
+
setParent,
|
|
1995
|
+
addEdge,
|
|
1996
|
+
removeEdge,
|
|
1997
|
+
getNode,
|
|
1998
|
+
getEdge,
|
|
1999
|
+
getState,
|
|
2000
|
+
childrenOf,
|
|
2001
|
+
edgesOf,
|
|
2002
|
+
anchorsOf,
|
|
2003
|
+
boundsOf,
|
|
2004
|
+
getNodeOBB,
|
|
2005
|
+
subscribe: subscribeLocal,
|
|
2006
|
+
transaction,
|
|
2007
|
+
pushHistory,
|
|
2008
|
+
undo,
|
|
2009
|
+
redo,
|
|
2010
|
+
canUndo,
|
|
2011
|
+
canRedo,
|
|
2012
|
+
clearHistory,
|
|
2013
|
+
clear
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/engines/graph/router.ts
|
|
2018
|
+
function routeOrthogonal(start, end, options) {
|
|
2019
|
+
const opts = typeof options === "string" ? { preferred: options } : options ?? {};
|
|
2020
|
+
const gridSize = opts.gridSize ?? 10;
|
|
2021
|
+
const stubLength = opts.stubLength ?? Math.max(20, gridSize);
|
|
2022
|
+
const stepPosition = opts.stepPosition ?? 0.5;
|
|
2023
|
+
let sExit = null;
|
|
2024
|
+
let sStub = null;
|
|
2025
|
+
let sCorner = null;
|
|
2026
|
+
let sSide = null;
|
|
2027
|
+
if (opts.sourceBounds) {
|
|
2028
|
+
const exitInfo = exitOnNearestSide(opts.sourceBounds, end, start);
|
|
2029
|
+
sExit = exitInfo.point;
|
|
2030
|
+
sSide = exitInfo.side;
|
|
2031
|
+
sStub = extrudePoint(exitInfo.point, exitInfo.side, stubLength);
|
|
2032
|
+
sCorner = orthoBridge(start, sExit, exitInfo.side);
|
|
2033
|
+
}
|
|
2034
|
+
let tExit = null;
|
|
2035
|
+
let tStub = null;
|
|
2036
|
+
let tCorner = null;
|
|
2037
|
+
let tSide = null;
|
|
2038
|
+
if (opts.targetBounds) {
|
|
2039
|
+
const enterInfo = exitOnNearestSide(opts.targetBounds, start, end);
|
|
2040
|
+
tExit = enterInfo.point;
|
|
2041
|
+
tSide = enterInfo.side;
|
|
2042
|
+
tStub = extrudePoint(enterInfo.point, enterInfo.side, stubLength);
|
|
2043
|
+
tCorner = orthoBridge(end, tExit, enterInfo.side);
|
|
2044
|
+
}
|
|
2045
|
+
const coreStart = sStub ?? start;
|
|
2046
|
+
const coreEnd = tStub ?? end;
|
|
2047
|
+
const obstacles = opts.obstacles ?? [];
|
|
2048
|
+
const filteredObstacles = obstacles.length > 0 && (opts.sourceBounds || opts.targetBounds) ? obstacles.filter((o) => o !== opts.sourceBounds && o !== opts.targetBounds) : obstacles;
|
|
2049
|
+
let core;
|
|
2050
|
+
if (filteredObstacles.length === 0) {
|
|
2051
|
+
if (sSide && tSide) {
|
|
2052
|
+
core = smoothStepRoute(coreStart, sSide, coreEnd, tSide, stepPosition);
|
|
2053
|
+
} else {
|
|
2054
|
+
const snap2 = opts.snap ?? gridSize;
|
|
2055
|
+
core = lRoute(coreStart, coreEnd, opts.preferred ?? "auto", snap2);
|
|
2056
|
+
}
|
|
2057
|
+
} else {
|
|
2058
|
+
const inflate = opts.inflate ?? 0;
|
|
2059
|
+
const maxNodes = opts.maxNodes ?? 5e3;
|
|
2060
|
+
const aStar = aStarRoute(coreStart, coreEnd, filteredObstacles, gridSize, inflate, maxNodes);
|
|
2061
|
+
core = aStar ?? lRoute(coreStart, coreEnd, opts.preferred ?? "auto", opts.snap ?? gridSize);
|
|
2062
|
+
}
|
|
2063
|
+
const points = [];
|
|
2064
|
+
pushIfDistinct(points, start);
|
|
2065
|
+
if (sCorner && !pointsEqual(sCorner, start)) pushIfDistinct(points, sCorner);
|
|
2066
|
+
if (sExit && !pointsEqual(sExit, points[points.length - 1])) pushIfDistinct(points, sExit);
|
|
2067
|
+
if (sStub && !pointsEqual(sStub, points[points.length - 1])) pushIfDistinct(points, sStub);
|
|
2068
|
+
for (let i = 0; i < core.length; i++) {
|
|
2069
|
+
const p = core[i];
|
|
2070
|
+
if (i === 0 && sStub && pointsEqual(p, sStub)) continue;
|
|
2071
|
+
if (i === core.length - 1 && tStub && pointsEqual(p, tStub)) continue;
|
|
2072
|
+
pushIfDistinct(points, p);
|
|
2073
|
+
}
|
|
2074
|
+
if (tStub && !pointsEqual(tStub, points[points.length - 1])) pushIfDistinct(points, tStub);
|
|
2075
|
+
if (tExit && !pointsEqual(tExit, points[points.length - 1])) pushIfDistinct(points, tExit);
|
|
2076
|
+
if (tCorner && !pointsEqual(tCorner, points[points.length - 1])) pushIfDistinct(points, tCorner);
|
|
2077
|
+
if (!pointsEqual(end, points[points.length - 1])) pushIfDistinct(points, end);
|
|
2078
|
+
return compressCollinear(points);
|
|
2079
|
+
}
|
|
2080
|
+
function smoothStepRoute(coreStart, sSide, coreEnd, tSide, stepPosition) {
|
|
2081
|
+
const [sx, sy] = coreStart;
|
|
2082
|
+
const [tx, ty] = coreEnd;
|
|
2083
|
+
const sAxis = sideAxis(sSide);
|
|
2084
|
+
const tAxis = sideAxis(tSide);
|
|
2085
|
+
const sDir = sideDir(sSide);
|
|
2086
|
+
const tDir = sideDir(tSide);
|
|
2087
|
+
if (sAxis === tAxis) {
|
|
2088
|
+
if (sDir + tDir === 0) {
|
|
2089
|
+
if (sAxis === "x") {
|
|
2090
|
+
const cx = sx + (tx - sx) * stepPosition;
|
|
2091
|
+
return [
|
|
2092
|
+
[sx, sy],
|
|
2093
|
+
[cx, sy],
|
|
2094
|
+
[cx, ty],
|
|
2095
|
+
[tx, ty]
|
|
2096
|
+
];
|
|
2097
|
+
}
|
|
2098
|
+
const cy = sy + (ty - sy) * stepPosition;
|
|
2099
|
+
return [
|
|
2100
|
+
[sx, sy],
|
|
2101
|
+
[sx, cy],
|
|
2102
|
+
[tx, cy],
|
|
2103
|
+
[tx, ty]
|
|
2104
|
+
];
|
|
2105
|
+
}
|
|
2106
|
+
if (sAxis === "x") {
|
|
2107
|
+
const extX = sDir > 0 ? Math.max(sx, tx) : Math.min(sx, tx);
|
|
2108
|
+
return [
|
|
2109
|
+
[sx, sy],
|
|
2110
|
+
[extX, sy],
|
|
2111
|
+
[extX, ty],
|
|
2112
|
+
[tx, ty]
|
|
2113
|
+
];
|
|
2114
|
+
}
|
|
2115
|
+
const extY = sDir > 0 ? Math.max(sy, ty) : Math.min(sy, ty);
|
|
2116
|
+
return [
|
|
2117
|
+
[sx, sy],
|
|
2118
|
+
[sx, extY],
|
|
2119
|
+
[tx, extY],
|
|
2120
|
+
[tx, ty]
|
|
2121
|
+
];
|
|
2122
|
+
}
|
|
2123
|
+
if (sAxis === "x") {
|
|
2124
|
+
return [
|
|
2125
|
+
[sx, sy],
|
|
2126
|
+
[tx, sy],
|
|
2127
|
+
[tx, ty]
|
|
2128
|
+
];
|
|
2129
|
+
}
|
|
2130
|
+
return [
|
|
2131
|
+
[sx, sy],
|
|
2132
|
+
[sx, ty],
|
|
2133
|
+
[tx, ty]
|
|
2134
|
+
];
|
|
2135
|
+
}
|
|
2136
|
+
function sideAxis(s) {
|
|
2137
|
+
return s === "left" || s === "right" ? "x" : "y";
|
|
2138
|
+
}
|
|
2139
|
+
function sideDir(s) {
|
|
2140
|
+
return s === "right" || s === "bottom" ? 1 : -1;
|
|
2141
|
+
}
|
|
2142
|
+
function pointsToPath(points) {
|
|
2143
|
+
if (points.length === 0) return "";
|
|
2144
|
+
const [head, ...rest] = points;
|
|
2145
|
+
return `M ${head[0]} ${head[1]} ${rest.map((p) => `L ${p[0]} ${p[1]}`).join(" ")}`;
|
|
2146
|
+
}
|
|
2147
|
+
function pointsToRoundedPath(points, radius) {
|
|
2148
|
+
if (points.length === 0) return "";
|
|
2149
|
+
if (points.length < 3 || radius <= 0) return pointsToPath(points);
|
|
2150
|
+
let d = `M ${points[0][0]} ${points[0][1]}`;
|
|
2151
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
2152
|
+
const a = points[i - 1];
|
|
2153
|
+
const b = points[i];
|
|
2154
|
+
const c = points[i + 1];
|
|
2155
|
+
const lenAB = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2156
|
+
const lenBC = Math.hypot(c[0] - b[0], c[1] - b[1]);
|
|
2157
|
+
const r = Math.min(radius, lenAB / 2, lenBC / 2);
|
|
2158
|
+
const ux = (b[0] - a[0]) / (lenAB || 1);
|
|
2159
|
+
const uy = (b[1] - a[1]) / (lenAB || 1);
|
|
2160
|
+
const vx = (c[0] - b[0]) / (lenBC || 1);
|
|
2161
|
+
const vy = (c[1] - b[1]) / (lenBC || 1);
|
|
2162
|
+
const cross = ux * vy - uy * vx;
|
|
2163
|
+
if (r < 0.5 || Math.abs(cross) < 1e-9) {
|
|
2164
|
+
d += ` L ${b[0]} ${b[1]}`;
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
const p1x = b[0] - ux * r;
|
|
2168
|
+
const p1y = b[1] - uy * r;
|
|
2169
|
+
const p2x = b[0] + vx * r;
|
|
2170
|
+
const p2y = b[1] + vy * r;
|
|
2171
|
+
d += ` L ${p1x} ${p1y} Q ${b[0]} ${b[1]} ${p2x} ${p2y}`;
|
|
2172
|
+
}
|
|
2173
|
+
const tail = points[points.length - 1];
|
|
2174
|
+
d += ` L ${tail[0]} ${tail[1]}`;
|
|
2175
|
+
return d;
|
|
2176
|
+
}
|
|
2177
|
+
function controlOffset(distance, curvature) {
|
|
2178
|
+
if (distance >= 0) return 0.5 * distance;
|
|
2179
|
+
return curvature * 25 * Math.sqrt(-distance);
|
|
2180
|
+
}
|
|
2181
|
+
function controlPoint(side, x1, y1, x2, y2, curvature) {
|
|
2182
|
+
switch (side) {
|
|
2183
|
+
case "left":
|
|
2184
|
+
return [x1 - controlOffset(x1 - x2, curvature), y1];
|
|
2185
|
+
case "right":
|
|
2186
|
+
return [x1 + controlOffset(x2 - x1, curvature), y1];
|
|
2187
|
+
case "top":
|
|
2188
|
+
return [x1, y1 - controlOffset(y1 - y2, curvature)];
|
|
2189
|
+
case "bottom":
|
|
2190
|
+
return [x1, y1 + controlOffset(y2 - y1, curvature)];
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
function getBezierPath(opts) {
|
|
2194
|
+
const curvature = opts.curvature ?? 0.25;
|
|
2195
|
+
const [sx, sy] = opts.source;
|
|
2196
|
+
const [tx, ty] = opts.target;
|
|
2197
|
+
const [c1x, c1y] = controlPoint(opts.sourceSide, sx, sy, tx, ty, curvature);
|
|
2198
|
+
const [c2x, c2y] = controlPoint(opts.targetSide, tx, ty, sx, sy, curvature);
|
|
2199
|
+
return `M ${sx} ${sy} C ${c1x} ${c1y} ${c2x} ${c2y} ${tx} ${ty}`;
|
|
2200
|
+
}
|
|
2201
|
+
function floatingAnchor(box, opposite) {
|
|
2202
|
+
const cx = box.x + box.w / 2;
|
|
2203
|
+
const cy = box.y + box.h / 2;
|
|
2204
|
+
const dx = opposite[0] - cx;
|
|
2205
|
+
const dy = opposite[1] - cy;
|
|
2206
|
+
if (dx === 0 && dy === 0) {
|
|
2207
|
+
return { point: [box.x + box.w, cy], side: "right" };
|
|
2208
|
+
}
|
|
2209
|
+
const hw = box.w / 2;
|
|
2210
|
+
const hh = box.h / 2;
|
|
2211
|
+
const tx = dx === 0 ? Infinity : hw / Math.abs(dx);
|
|
2212
|
+
const ty = dy === 0 ? Infinity : hh / Math.abs(dy);
|
|
2213
|
+
if (tx < ty) {
|
|
2214
|
+
const sign2 = dx > 0 ? 1 : -1;
|
|
2215
|
+
return {
|
|
2216
|
+
point: [cx + sign2 * hw, cy + sign2 * (hw / Math.abs(dx)) * dy],
|
|
2217
|
+
side: sign2 > 0 ? "right" : "left"
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
const sign = dy > 0 ? 1 : -1;
|
|
2221
|
+
return {
|
|
2222
|
+
point: [cx + sign * (hh / Math.abs(dy)) * dx, cy + sign * hh],
|
|
2223
|
+
side: sign > 0 ? "bottom" : "top"
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
function nearestSide(box, target) {
|
|
2227
|
+
const cx = box.x + box.w / 2;
|
|
2228
|
+
const cy = box.y + box.h / 2;
|
|
2229
|
+
const dx = target[0] - cx;
|
|
2230
|
+
const dy = target[1] - cy;
|
|
2231
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
|
|
2232
|
+
return dy >= 0 ? "bottom" : "top";
|
|
2233
|
+
}
|
|
2234
|
+
function getNodeIntersection(intersectionBox, targetCenter) {
|
|
2235
|
+
const w = intersectionBox.w / 2;
|
|
2236
|
+
const h = intersectionBox.h / 2;
|
|
2237
|
+
const x2 = intersectionBox.x + w;
|
|
2238
|
+
const y2 = intersectionBox.y + h;
|
|
2239
|
+
if (w === 0 || h === 0) return [x2, y2];
|
|
2240
|
+
const x1 = targetCenter[0];
|
|
2241
|
+
const y1 = targetCenter[1];
|
|
2242
|
+
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
|
|
2243
|
+
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
|
|
2244
|
+
const denom = Math.abs(xx1) + Math.abs(yy1);
|
|
2245
|
+
if (denom === 0) {
|
|
2246
|
+
return [intersectionBox.x + intersectionBox.w, y2];
|
|
2247
|
+
}
|
|
2248
|
+
const a = 1 / denom;
|
|
2249
|
+
const xx3 = a * xx1;
|
|
2250
|
+
const yy3 = a * yy1;
|
|
2251
|
+
const x = w * (xx3 + yy3) + x2;
|
|
2252
|
+
const y = h * (-xx3 + yy3) + y2;
|
|
2253
|
+
return [x, y];
|
|
2254
|
+
}
|
|
2255
|
+
function getEdgePosition(box, intersectionPoint) {
|
|
2256
|
+
const nx = Math.round(box.x);
|
|
2257
|
+
const ny = Math.round(box.y);
|
|
2258
|
+
const px = Math.round(intersectionPoint[0]);
|
|
2259
|
+
const py = Math.round(intersectionPoint[1]);
|
|
2260
|
+
if (px <= nx + 1) return "left";
|
|
2261
|
+
if (px >= nx + box.w - 1) return "right";
|
|
2262
|
+
if (py <= ny + 1) return "top";
|
|
2263
|
+
if (py >= ny + box.h - 1) return "bottom";
|
|
2264
|
+
return "top";
|
|
2265
|
+
}
|
|
2266
|
+
function resolveEdgeEndpoint(sourceBox, targetBox, padding = 0) {
|
|
2267
|
+
const targetCenter = [targetBox.x + targetBox.w / 2, targetBox.y + targetBox.h / 2];
|
|
2268
|
+
const raw = getNodeIntersection(sourceBox, targetCenter);
|
|
2269
|
+
const side = getEdgePosition(sourceBox, raw);
|
|
2270
|
+
if (padding === 0) return { point: raw, side };
|
|
2271
|
+
let point = raw;
|
|
2272
|
+
switch (side) {
|
|
2273
|
+
case "right":
|
|
2274
|
+
point = [raw[0] + padding, raw[1]];
|
|
2275
|
+
break;
|
|
2276
|
+
case "left":
|
|
2277
|
+
point = [raw[0] - padding, raw[1]];
|
|
2278
|
+
break;
|
|
2279
|
+
case "bottom":
|
|
2280
|
+
point = [raw[0], raw[1] + padding];
|
|
2281
|
+
break;
|
|
2282
|
+
case "top":
|
|
2283
|
+
point = [raw[0], raw[1] - padding];
|
|
2284
|
+
break;
|
|
2285
|
+
}
|
|
2286
|
+
return { point, side };
|
|
2287
|
+
}
|
|
2288
|
+
function inferSide(box, anchor, opposite, tolerance = 1) {
|
|
2289
|
+
const onLeft = Math.abs(anchor[0] - box.x) <= tolerance;
|
|
2290
|
+
const onRight = Math.abs(anchor[0] - (box.x + box.w)) <= tolerance;
|
|
2291
|
+
const onTop = Math.abs(anchor[1] - box.y) <= tolerance;
|
|
2292
|
+
const onBottom = Math.abs(anchor[1] - (box.y + box.h)) <= tolerance;
|
|
2293
|
+
const horizontalHit = onLeft || onRight;
|
|
2294
|
+
const verticalHit = onTop || onBottom;
|
|
2295
|
+
if (horizontalHit && verticalHit) {
|
|
2296
|
+
const cx = box.x + box.w / 2;
|
|
2297
|
+
const cy = box.y + box.h / 2;
|
|
2298
|
+
const dx = opposite[0] - cx;
|
|
2299
|
+
const dy = opposite[1] - cy;
|
|
2300
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
2301
|
+
return { side: onRight ? "right" : "left", confident: true };
|
|
2302
|
+
}
|
|
2303
|
+
return { side: onBottom ? "bottom" : "top", confident: true };
|
|
2304
|
+
}
|
|
2305
|
+
if (onLeft) return { side: "left", confident: true };
|
|
2306
|
+
if (onRight) return { side: "right", confident: true };
|
|
2307
|
+
if (onTop) return { side: "top", confident: true };
|
|
2308
|
+
if (onBottom) return { side: "bottom", confident: true };
|
|
2309
|
+
return { side: nearestSide(box, opposite), confident: false };
|
|
2310
|
+
}
|
|
2311
|
+
function exitOnNearestSide(box, target, anchor) {
|
|
2312
|
+
const cx = box.x + box.w / 2;
|
|
2313
|
+
const cy = box.y + box.h / 2;
|
|
2314
|
+
if (anchor) {
|
|
2315
|
+
const tolerance = 1;
|
|
2316
|
+
const onLeft = Math.abs(anchor[0] - box.x) <= tolerance;
|
|
2317
|
+
const onRight = Math.abs(anchor[0] - (box.x + box.w)) <= tolerance;
|
|
2318
|
+
const onTop = Math.abs(anchor[1] - box.y) <= tolerance;
|
|
2319
|
+
const onBottom = Math.abs(anchor[1] - (box.y + box.h)) <= tolerance;
|
|
2320
|
+
const horizontalHit = onLeft || onRight;
|
|
2321
|
+
const verticalHit = onTop || onBottom;
|
|
2322
|
+
if (horizontalHit && verticalHit) {
|
|
2323
|
+
const dx2 = target[0] - cx;
|
|
2324
|
+
const dy2 = target[1] - cy;
|
|
2325
|
+
if (Math.abs(dx2) >= Math.abs(dy2)) {
|
|
2326
|
+
return onRight ? { point: [box.x + box.w, cy], side: "right" } : { point: [box.x, cy], side: "left" };
|
|
2327
|
+
}
|
|
2328
|
+
return onBottom ? { point: [cx, box.y + box.h], side: "bottom" } : { point: [cx, box.y], side: "top" };
|
|
2329
|
+
}
|
|
2330
|
+
if (onRight) return { point: [box.x + box.w, cy], side: "right" };
|
|
2331
|
+
if (onLeft) return { point: [box.x, cy], side: "left" };
|
|
2332
|
+
if (onBottom) return { point: [cx, box.y + box.h], side: "bottom" };
|
|
2333
|
+
if (onTop) return { point: [cx, box.y], side: "top" };
|
|
2334
|
+
}
|
|
2335
|
+
const dx = target[0] - cx;
|
|
2336
|
+
const dy = target[1] - cy;
|
|
2337
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
2338
|
+
if (dx >= 0) return { point: [box.x + box.w, cy], side: "right" };
|
|
2339
|
+
return { point: [box.x, cy], side: "left" };
|
|
2340
|
+
}
|
|
2341
|
+
if (dy >= 0) return { point: [cx, box.y + box.h], side: "bottom" };
|
|
2342
|
+
return { point: [cx, box.y], side: "top" };
|
|
2343
|
+
}
|
|
2344
|
+
function extrudePoint(point, side, distance) {
|
|
2345
|
+
switch (side) {
|
|
2346
|
+
case "top":
|
|
2347
|
+
return [point[0], point[1] - distance];
|
|
2348
|
+
case "right":
|
|
2349
|
+
return [point[0] + distance, point[1]];
|
|
2350
|
+
case "bottom":
|
|
2351
|
+
return [point[0], point[1] + distance];
|
|
2352
|
+
case "left":
|
|
2353
|
+
return [point[0] - distance, point[1]];
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function orthoBridge(start, exit, side) {
|
|
2357
|
+
switch (side) {
|
|
2358
|
+
case "right":
|
|
2359
|
+
case "left":
|
|
2360
|
+
return [exit[0], start[1]];
|
|
2361
|
+
case "top":
|
|
2362
|
+
case "bottom":
|
|
2363
|
+
return [start[0], exit[1]];
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function pointsEqual(a, b) {
|
|
2367
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
2368
|
+
}
|
|
2369
|
+
function pushIfDistinct(arr, p) {
|
|
2370
|
+
if (arr.length === 0) {
|
|
2371
|
+
arr.push(p);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const last = arr[arr.length - 1];
|
|
2375
|
+
if (!pointsEqual(last, p)) arr.push(p);
|
|
2376
|
+
}
|
|
2377
|
+
function compressCollinear(points) {
|
|
2378
|
+
if (points.length <= 2) return points.slice();
|
|
2379
|
+
const out = [points[0], points[1]];
|
|
2380
|
+
for (let i = 2; i < points.length; i++) {
|
|
2381
|
+
const cur = points[i];
|
|
2382
|
+
const a = out[out.length - 2];
|
|
2383
|
+
const b = out[out.length - 1];
|
|
2384
|
+
const dx1 = b[0] - a[0];
|
|
2385
|
+
const dy1 = b[1] - a[1];
|
|
2386
|
+
const dx2 = cur[0] - b[0];
|
|
2387
|
+
const dy2 = cur[1] - b[1];
|
|
2388
|
+
const cross = dx1 * dy2 - dy1 * dx2;
|
|
2389
|
+
if (cross === 0 && Math.sign(dx1) === Math.sign(dx2) && Math.sign(dy1) === Math.sign(dy2)) {
|
|
2390
|
+
out[out.length - 1] = cur;
|
|
2391
|
+
} else {
|
|
2392
|
+
out.push(cur);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return out;
|
|
2396
|
+
}
|
|
2397
|
+
function lRoute(start, end, preferred, _snapStep) {
|
|
2398
|
+
const [sx, sy] = start;
|
|
2399
|
+
const [ex, ey] = end;
|
|
2400
|
+
if (sx === ex || sy === ey) return [start, end];
|
|
2401
|
+
const dx = Math.abs(ex - sx);
|
|
2402
|
+
const dy = Math.abs(ey - sy);
|
|
2403
|
+
const useHV = preferred === "hv" ? true : preferred === "vh" ? false : dx >= dy;
|
|
2404
|
+
if (useHV) return [start, [ex, sy], end];
|
|
2405
|
+
return [start, [sx, ey], end];
|
|
2406
|
+
}
|
|
2407
|
+
function inflateObstacles(obstacles, pad) {
|
|
2408
|
+
return obstacles.map((o) => ({
|
|
2409
|
+
x0: o.x - pad,
|
|
2410
|
+
y0: o.y - pad,
|
|
2411
|
+
x1: o.x + o.w + pad,
|
|
2412
|
+
y1: o.y + o.h + pad
|
|
2413
|
+
}));
|
|
2414
|
+
}
|
|
2415
|
+
function pointBlocked(x, y, rects) {
|
|
2416
|
+
for (const r of rects) {
|
|
2417
|
+
if (x > r.x0 && x < r.x1 && y > r.y0 && y < r.y1) return true;
|
|
2418
|
+
}
|
|
2419
|
+
return false;
|
|
2420
|
+
}
|
|
2421
|
+
function snap(value, step) {
|
|
2422
|
+
return Math.round(value / step) * step;
|
|
2423
|
+
}
|
|
2424
|
+
function key(x, y) {
|
|
2425
|
+
return `${x},${y}`;
|
|
2426
|
+
}
|
|
2427
|
+
function manhattan(ax, ay, bx, by) {
|
|
2428
|
+
return Math.abs(ax - bx) + Math.abs(ay - by);
|
|
2429
|
+
}
|
|
2430
|
+
function reconstruct(cameFrom, endKey) {
|
|
2431
|
+
const reversed = [];
|
|
2432
|
+
let cursor = endKey;
|
|
2433
|
+
while (cursor !== void 0) {
|
|
2434
|
+
const [xs, ys] = cursor.split(",");
|
|
2435
|
+
reversed.push([Number(xs), Number(ys)]);
|
|
2436
|
+
cursor = cameFrom.get(cursor);
|
|
2437
|
+
}
|
|
2438
|
+
reversed.reverse();
|
|
2439
|
+
const out = [];
|
|
2440
|
+
for (let i = 0; i < reversed.length; i++) {
|
|
2441
|
+
const cur = reversed[i];
|
|
2442
|
+
if (out.length < 2) {
|
|
2443
|
+
out.push(cur);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
const a = out[out.length - 2];
|
|
2447
|
+
const b = out[out.length - 1];
|
|
2448
|
+
const dx1 = b[0] - a[0];
|
|
2449
|
+
const dy1 = b[1] - a[1];
|
|
2450
|
+
const dx2 = cur[0] - b[0];
|
|
2451
|
+
const dy2 = cur[1] - b[1];
|
|
2452
|
+
if (Math.sign(dx1) === Math.sign(dx2) && Math.sign(dy1) === Math.sign(dy2)) {
|
|
2453
|
+
out[out.length - 1] = cur;
|
|
2454
|
+
} else {
|
|
2455
|
+
out.push(cur);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return out;
|
|
2459
|
+
}
|
|
2460
|
+
var MinHeap = class {
|
|
2461
|
+
data = [];
|
|
2462
|
+
get size() {
|
|
2463
|
+
return this.data.length;
|
|
2464
|
+
}
|
|
2465
|
+
push(entry) {
|
|
2466
|
+
this.data.push(entry);
|
|
2467
|
+
this.bubbleUp(this.data.length - 1);
|
|
2468
|
+
}
|
|
2469
|
+
pop() {
|
|
2470
|
+
if (this.data.length === 0) return void 0;
|
|
2471
|
+
const head = this.data[0];
|
|
2472
|
+
const tail = this.data.pop();
|
|
2473
|
+
if (this.data.length > 0) {
|
|
2474
|
+
this.data[0] = tail;
|
|
2475
|
+
this.bubbleDown(0);
|
|
2476
|
+
}
|
|
2477
|
+
return head;
|
|
2478
|
+
}
|
|
2479
|
+
bubbleUp(i) {
|
|
2480
|
+
while (i > 0) {
|
|
2481
|
+
const parent = i - 1 >> 1;
|
|
2482
|
+
if (this.data[i].f < this.data[parent].f) {
|
|
2483
|
+
const tmp = this.data[i];
|
|
2484
|
+
this.data[i] = this.data[parent];
|
|
2485
|
+
this.data[parent] = tmp;
|
|
2486
|
+
i = parent;
|
|
2487
|
+
} else {
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
bubbleDown(i) {
|
|
2493
|
+
const n = this.data.length;
|
|
2494
|
+
while (true) {
|
|
2495
|
+
const l = i * 2 + 1;
|
|
2496
|
+
const r = l + 1;
|
|
2497
|
+
let smallest = i;
|
|
2498
|
+
if (l < n && this.data[l].f < this.data[smallest].f) smallest = l;
|
|
2499
|
+
if (r < n && this.data[r].f < this.data[smallest].f) smallest = r;
|
|
2500
|
+
if (smallest === i) return;
|
|
2501
|
+
const tmp = this.data[i];
|
|
2502
|
+
this.data[i] = this.data[smallest];
|
|
2503
|
+
this.data[smallest] = tmp;
|
|
2504
|
+
i = smallest;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
function aStarRoute(start, end, obstacles, gridSize, inflate, maxNodes) {
|
|
2509
|
+
const rects = inflateObstacles(obstacles, inflate);
|
|
2510
|
+
const sx = snap(start[0], gridSize);
|
|
2511
|
+
const sy = snap(start[1], gridSize);
|
|
2512
|
+
const ex = snap(end[0], gridSize);
|
|
2513
|
+
const ey = snap(end[1], gridSize);
|
|
2514
|
+
if (pointBlocked(sx, sy, rects) || pointBlocked(ex, ey, rects)) return null;
|
|
2515
|
+
const startKey = key(sx, sy);
|
|
2516
|
+
const endKey = key(ex, ey);
|
|
2517
|
+
const gScore = /* @__PURE__ */ new Map([[startKey, 0]]);
|
|
2518
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
2519
|
+
const cameDir = /* @__PURE__ */ new Map([[startKey, null]]);
|
|
2520
|
+
const open = new MinHeap();
|
|
2521
|
+
open.push({ k: startKey, x: sx, y: sy, f: manhattan(sx, sy, ex, ey) });
|
|
2522
|
+
const TURN_PENALTY = gridSize;
|
|
2523
|
+
let expansions = 0;
|
|
2524
|
+
while (open.size > 0) {
|
|
2525
|
+
if (expansions++ > maxNodes) return null;
|
|
2526
|
+
const current = open.pop();
|
|
2527
|
+
const bestG = gScore.get(current.k) ?? Infinity;
|
|
2528
|
+
const expectedF = bestG + manhattan(current.x, current.y, ex, ey);
|
|
2529
|
+
if (current.f > expectedF) continue;
|
|
2530
|
+
if (current.k === endKey) {
|
|
2531
|
+
const path = reconstruct(cameFrom, endKey);
|
|
2532
|
+
if (path.length > 0) {
|
|
2533
|
+
path[0] = start;
|
|
2534
|
+
path[path.length - 1] = end;
|
|
2535
|
+
}
|
|
2536
|
+
return path;
|
|
2537
|
+
}
|
|
2538
|
+
const incomingDir = cameDir.get(current.k) ?? null;
|
|
2539
|
+
const neighbours = [
|
|
2540
|
+
[gridSize, 0, "h"],
|
|
2541
|
+
[-gridSize, 0, "h"],
|
|
2542
|
+
[0, gridSize, "v"],
|
|
2543
|
+
[0, -gridSize, "v"]
|
|
2544
|
+
];
|
|
2545
|
+
for (const [dx, dy, dir] of neighbours) {
|
|
2546
|
+
const nx = current.x + dx;
|
|
2547
|
+
const ny = current.y + dy;
|
|
2548
|
+
if (pointBlocked(nx, ny, rects)) continue;
|
|
2549
|
+
const turn = incomingDir !== null && incomingDir !== dir;
|
|
2550
|
+
const tentativeG = bestG + gridSize + (turn ? TURN_PENALTY : 0);
|
|
2551
|
+
const nk = key(nx, ny);
|
|
2552
|
+
const prevG = gScore.get(nk) ?? Infinity;
|
|
2553
|
+
if (tentativeG < prevG) {
|
|
2554
|
+
cameFrom.set(nk, current.k);
|
|
2555
|
+
cameDir.set(nk, dir);
|
|
2556
|
+
gScore.set(nk, tentativeG);
|
|
2557
|
+
const f = tentativeG + manhattan(nx, ny, ex, ey);
|
|
2558
|
+
open.push({ k: nk, x: nx, y: ny, f });
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/engines/graph/obb.ts
|
|
2566
|
+
function obbCorners(obb) {
|
|
2567
|
+
const cos = Math.cos(obb.angle);
|
|
2568
|
+
const sin = Math.sin(obb.angle);
|
|
2569
|
+
const { halfWidth: hw, halfHeight: hh } = obb;
|
|
2570
|
+
const [cx, cy] = obb.center;
|
|
2571
|
+
const lx = [-hw, hw, hw, -hw];
|
|
2572
|
+
const ly = [-hh, -hh, hh, hh];
|
|
2573
|
+
const out = [];
|
|
2574
|
+
for (let i = 0; i < 4; i++) {
|
|
2575
|
+
const x = lx[i];
|
|
2576
|
+
const y = ly[i];
|
|
2577
|
+
out.push([cx + x * cos - y * sin, cy + x * sin + y * cos]);
|
|
2578
|
+
}
|
|
2579
|
+
return out;
|
|
2580
|
+
}
|
|
2581
|
+
function aabbFromOBB(obb) {
|
|
2582
|
+
const corners = obbCorners(obb);
|
|
2583
|
+
let x0 = corners[0][0];
|
|
2584
|
+
let x1 = corners[0][0];
|
|
2585
|
+
let y0 = corners[0][1];
|
|
2586
|
+
let y1 = corners[0][1];
|
|
2587
|
+
for (let i = 1; i < 4; i++) {
|
|
2588
|
+
const [x, y] = corners[i];
|
|
2589
|
+
if (x < x0) x0 = x;
|
|
2590
|
+
if (x > x1) x1 = x;
|
|
2591
|
+
if (y < y0) y0 = y;
|
|
2592
|
+
if (y > y1) y1 = y;
|
|
2593
|
+
}
|
|
2594
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
2595
|
+
}
|
|
2596
|
+
function obbContainsPoint(obb, p) {
|
|
2597
|
+
const cos = Math.cos(-obb.angle);
|
|
2598
|
+
const sin = Math.sin(-obb.angle);
|
|
2599
|
+
const dx = p[0] - obb.center[0];
|
|
2600
|
+
const dy = p[1] - obb.center[1];
|
|
2601
|
+
const lx = dx * cos - dy * sin;
|
|
2602
|
+
const ly = dx * sin + dy * cos;
|
|
2603
|
+
return Math.abs(lx) <= obb.halfWidth && Math.abs(ly) <= obb.halfHeight;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/engines/table/filter.ts
|
|
2607
|
+
function resolveOperator(filter) {
|
|
2608
|
+
const adv = filter;
|
|
2609
|
+
if (adv.op != null) return adv.op;
|
|
2610
|
+
const col = filter;
|
|
2611
|
+
if (col.operator != null) return col.operator;
|
|
2612
|
+
return Array.isArray(filter.value) ? "in" : "eq";
|
|
2613
|
+
}
|
|
2614
|
+
function isEmptyValue(val) {
|
|
2615
|
+
return val == null || val === "";
|
|
2616
|
+
}
|
|
2617
|
+
function asNumberPair(a, b) {
|
|
2618
|
+
const na = typeof a === "number" ? a : Number(a);
|
|
2619
|
+
const nb = typeof b === "number" ? b : Number(b);
|
|
2620
|
+
if (Number.isNaN(na) || Number.isNaN(nb)) return null;
|
|
2621
|
+
return [na, nb];
|
|
2622
|
+
}
|
|
2623
|
+
function matchesOperator(val, op, filterValue) {
|
|
2624
|
+
switch (op) {
|
|
2625
|
+
case "eq":
|
|
2626
|
+
return val === filterValue;
|
|
2627
|
+
case "neq":
|
|
2628
|
+
return val !== filterValue;
|
|
2629
|
+
case "lt":
|
|
2630
|
+
case "lte":
|
|
2631
|
+
case "gt":
|
|
2632
|
+
case "gte": {
|
|
2633
|
+
const pair = asNumberPair(val, filterValue);
|
|
2634
|
+
if (!pair) return false;
|
|
2635
|
+
const [a, b] = pair;
|
|
2636
|
+
if (op === "lt") return a < b;
|
|
2637
|
+
if (op === "lte") return a <= b;
|
|
2638
|
+
if (op === "gt") return a > b;
|
|
2639
|
+
return a >= b;
|
|
2640
|
+
}
|
|
2641
|
+
case "between": {
|
|
2642
|
+
if (!Array.isArray(filterValue) || filterValue.length !== 2) return false;
|
|
2643
|
+
const minPair = asNumberPair(val, filterValue[0]);
|
|
2644
|
+
const maxPair = asNumberPair(val, filterValue[1]);
|
|
2645
|
+
if (!minPair || !maxPair) return false;
|
|
2646
|
+
return minPair[0] >= minPair[1] && maxPair[0] <= maxPair[1];
|
|
2647
|
+
}
|
|
2648
|
+
case "in":
|
|
2649
|
+
return Array.isArray(filterValue) && filterValue.includes(val);
|
|
2650
|
+
case "notIn":
|
|
2651
|
+
return Array.isArray(filterValue) && !filterValue.includes(val);
|
|
2652
|
+
case "contains":
|
|
2653
|
+
if (typeof val !== "string") return false;
|
|
2654
|
+
return val.toLowerCase().includes(String(filterValue ?? "").toLowerCase());
|
|
2655
|
+
case "startsWith":
|
|
2656
|
+
if (typeof val !== "string") return false;
|
|
2657
|
+
return val.toLowerCase().startsWith(String(filterValue ?? "").toLowerCase());
|
|
2658
|
+
case "endsWith":
|
|
2659
|
+
if (typeof val !== "string") return false;
|
|
2660
|
+
return val.toLowerCase().endsWith(String(filterValue ?? "").toLowerCase());
|
|
2661
|
+
case "isEmpty":
|
|
2662
|
+
return isEmptyValue(val);
|
|
2663
|
+
case "isNotEmpty":
|
|
2664
|
+
return !isEmptyValue(val);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
function matchesColumnFilter(row, filter) {
|
|
2668
|
+
const op = resolveOperator(filter);
|
|
2669
|
+
return matchesOperator(row[filter.column], op, filter.value);
|
|
2670
|
+
}
|
|
2671
|
+
function computeAggregate(values, type) {
|
|
2672
|
+
if (typeof type === "function") return type(values);
|
|
2673
|
+
if (type === "count") return values.length;
|
|
2674
|
+
const nums = [];
|
|
2675
|
+
for (const v of values) {
|
|
2676
|
+
if (v == null || v === "") continue;
|
|
2677
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
2678
|
+
if (!Number.isNaN(n)) nums.push(n);
|
|
2679
|
+
}
|
|
2680
|
+
if (nums.length === 0) return null;
|
|
2681
|
+
switch (type) {
|
|
2682
|
+
case "sum":
|
|
2683
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
2684
|
+
case "avg":
|
|
2685
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
2686
|
+
case "min":
|
|
2687
|
+
return Math.min(...nums);
|
|
2688
|
+
case "max":
|
|
2689
|
+
return Math.max(...nums);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// src/engines/table/TableEngine.ts
|
|
2694
|
+
function statePath(tableId, key2) {
|
|
2695
|
+
return `${TABLE_PREFIX}${tableId}.state.${key2}`;
|
|
2696
|
+
}
|
|
2697
|
+
var tableCounter = 0;
|
|
2698
|
+
function createTable(core, options) {
|
|
2699
|
+
const tableId = `t${tableCounter++}`;
|
|
2700
|
+
let currentPageSize = options?.pageSize ?? 50;
|
|
2701
|
+
let order = [];
|
|
2702
|
+
let filteredOrder = [];
|
|
2703
|
+
let sorts = [];
|
|
2704
|
+
let filters = [];
|
|
2705
|
+
let customFilterFn = null;
|
|
2706
|
+
let page = 1;
|
|
2707
|
+
let pinnedLeft = [];
|
|
2708
|
+
let pinnedRight = [];
|
|
2709
|
+
let columnWidths = {};
|
|
2710
|
+
let groupByField = null;
|
|
2711
|
+
let columnAggregates = [];
|
|
2712
|
+
let groups = [];
|
|
2713
|
+
const dataById = /* @__PURE__ */ new Map();
|
|
2714
|
+
const keysById = /* @__PURE__ */ new Map();
|
|
2715
|
+
function getRowData(id) {
|
|
2716
|
+
const stored = dataById.get(id);
|
|
2717
|
+
if (!stored) return void 0;
|
|
2718
|
+
if (options?.columns) {
|
|
2719
|
+
const filtered = {};
|
|
2720
|
+
for (const col of options.columns) {
|
|
2721
|
+
filtered[col] = stored[col];
|
|
2722
|
+
}
|
|
2723
|
+
return filtered;
|
|
2724
|
+
}
|
|
2725
|
+
return stored;
|
|
2726
|
+
}
|
|
2727
|
+
function matchesFilters(rowData) {
|
|
2728
|
+
if (customFilterFn && !customFilterFn(rowData)) return false;
|
|
2729
|
+
for (const f of filters) {
|
|
2730
|
+
if (!matchesColumnFilter(rowData, f)) return false;
|
|
2731
|
+
}
|
|
2732
|
+
return true;
|
|
2733
|
+
}
|
|
2734
|
+
function compareBySort(a, b) {
|
|
2735
|
+
const aData = getRowData(a);
|
|
2736
|
+
const bData = getRowData(b);
|
|
2737
|
+
for (const s of sorts) {
|
|
2738
|
+
const aVal = aData?.[s.column];
|
|
2739
|
+
const bVal = bData?.[s.column];
|
|
2740
|
+
if (aVal === bVal) continue;
|
|
2741
|
+
if (aVal === void 0 || aVal === null) return 1;
|
|
2742
|
+
if (bVal === void 0 || bVal === null) return -1;
|
|
2743
|
+
const cmp = aVal < bVal ? -1 : 1;
|
|
2744
|
+
return s.direction === "asc" ? cmp : -cmp;
|
|
2745
|
+
}
|
|
2746
|
+
return 0;
|
|
2747
|
+
}
|
|
2748
|
+
function recomputeFiltered() {
|
|
2749
|
+
if (filters.length === 0 && !customFilterFn) {
|
|
2750
|
+
filteredOrder = [...order];
|
|
2751
|
+
} else {
|
|
2752
|
+
filteredOrder = order.filter((id) => {
|
|
2753
|
+
const data = getRowData(id);
|
|
2754
|
+
return data ? matchesFilters(data) : false;
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
if (sorts.length > 0) {
|
|
2758
|
+
filteredOrder.sort(compareBySort);
|
|
2759
|
+
}
|
|
2760
|
+
const totalPages = Math.max(1, Math.ceil(filteredOrder.length / currentPageSize));
|
|
2761
|
+
if (page > totalPages) page = totalPages;
|
|
2762
|
+
recomputeGroups();
|
|
2763
|
+
publishState();
|
|
2764
|
+
}
|
|
2765
|
+
function recomputeGroups() {
|
|
2766
|
+
if (!groupByField) {
|
|
2767
|
+
groups = [];
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const map = /* @__PURE__ */ new Map();
|
|
2771
|
+
const orderKeys = [];
|
|
2772
|
+
for (const id of filteredOrder) {
|
|
2773
|
+
const data = getRowData(id);
|
|
2774
|
+
if (!data) continue;
|
|
2775
|
+
const raw = data[groupByField];
|
|
2776
|
+
const key2 = raw == null ? "__null__" : String(raw);
|
|
2777
|
+
if (!map.has(key2)) {
|
|
2778
|
+
map.set(key2, { value: raw, rows: [] });
|
|
2779
|
+
orderKeys.push(key2);
|
|
2780
|
+
}
|
|
2781
|
+
map.get(key2).rows.push(id);
|
|
2782
|
+
}
|
|
2783
|
+
groups = orderKeys.map((key2) => {
|
|
2784
|
+
const { value, rows } = map.get(key2);
|
|
2785
|
+
const aggregates = {};
|
|
2786
|
+
for (const agg of columnAggregates) {
|
|
2787
|
+
const values = rows.map((id) => dataById.get(id)?.[agg.column]);
|
|
2788
|
+
aggregates[agg.column] = computeAggregate(values, agg.type);
|
|
2789
|
+
}
|
|
2790
|
+
return { key: key2, value, rows, aggregates };
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
function publishState() {
|
|
2794
|
+
const totalPages = Math.max(1, Math.ceil(filteredOrder.length / currentPageSize));
|
|
2795
|
+
core.set(statePath(tableId, "totalRows"), order.length);
|
|
2796
|
+
core.set(statePath(tableId, "filteredRows"), filteredOrder.length);
|
|
2797
|
+
core.set(statePath(tableId, "page"), page);
|
|
2798
|
+
core.set(statePath(tableId, "pageSize"), currentPageSize);
|
|
2799
|
+
core.set(statePath(tableId, "totalPages"), totalPages);
|
|
2800
|
+
core.set(statePath(tableId, "groupBy"), groupByField);
|
|
2801
|
+
core.set(statePath(tableId, "groupCount"), groups.length);
|
|
2802
|
+
}
|
|
2803
|
+
function getColumnKeys() {
|
|
2804
|
+
if (options?.columns) return options.columns;
|
|
2805
|
+
if (order.length === 0) return [];
|
|
2806
|
+
const firstId = order[0];
|
|
2807
|
+
return keysById.get(firstId) ?? [];
|
|
2808
|
+
}
|
|
2809
|
+
function toColumnFilter(column, filter) {
|
|
2810
|
+
const adv = filter;
|
|
2811
|
+
if (adv.op != null) {
|
|
2812
|
+
return { column, operator: adv.op, value: adv.value };
|
|
2813
|
+
}
|
|
2814
|
+
const cf = filter;
|
|
2815
|
+
return { column, operator: cf.operator, value: cf.value };
|
|
2816
|
+
}
|
|
2817
|
+
const table = {
|
|
2818
|
+
addRow(id, data) {
|
|
2819
|
+
dataById.set(id, data);
|
|
2820
|
+
keysById.set(id, Object.keys(data));
|
|
2821
|
+
order.push(id);
|
|
2822
|
+
recomputeFiltered();
|
|
2823
|
+
},
|
|
2824
|
+
addRows(rows) {
|
|
2825
|
+
for (const row of rows) {
|
|
2826
|
+
dataById.set(row.id, row.data);
|
|
2827
|
+
keysById.set(row.id, Object.keys(row.data));
|
|
2828
|
+
order.push(row.id);
|
|
2829
|
+
}
|
|
2830
|
+
recomputeFiltered();
|
|
2831
|
+
},
|
|
2832
|
+
removeRow(id) {
|
|
2833
|
+
const idx = order.indexOf(id);
|
|
2834
|
+
if (idx === -1) return;
|
|
2835
|
+
order.splice(idx, 1);
|
|
2836
|
+
dataById.delete(id);
|
|
2837
|
+
keysById.delete(id);
|
|
2838
|
+
recomputeFiltered();
|
|
2839
|
+
},
|
|
2840
|
+
updateCell(id, column, value) {
|
|
2841
|
+
const row = dataById.get(id);
|
|
2842
|
+
if (!row) return;
|
|
2843
|
+
row[column] = value;
|
|
2844
|
+
const keys = keysById.get(id);
|
|
2845
|
+
if (keys && !keys.includes(column)) {
|
|
2846
|
+
keys.push(column);
|
|
2847
|
+
}
|
|
2848
|
+
recomputeFiltered();
|
|
2849
|
+
},
|
|
2850
|
+
getRow(id) {
|
|
2851
|
+
return getRowData(id);
|
|
2852
|
+
},
|
|
2853
|
+
getCell(id, column) {
|
|
2854
|
+
return dataById.get(id)?.[column];
|
|
2855
|
+
},
|
|
2856
|
+
getVisibleRows() {
|
|
2857
|
+
const start = (page - 1) * currentPageSize;
|
|
2858
|
+
const end = start + currentPageSize;
|
|
2859
|
+
const slice = filteredOrder.slice(start, end);
|
|
2860
|
+
const out = new Array(slice.length);
|
|
2861
|
+
for (let i = 0; i < slice.length; i++) {
|
|
2862
|
+
const id = slice[i];
|
|
2863
|
+
out[i] = { id, data: getRowData(id) ?? {} };
|
|
2864
|
+
}
|
|
2865
|
+
return out;
|
|
2866
|
+
},
|
|
2867
|
+
getAllRowIds() {
|
|
2868
|
+
return [...order];
|
|
2869
|
+
},
|
|
2870
|
+
getAllRows() {
|
|
2871
|
+
const out = new Array(filteredOrder.length);
|
|
2872
|
+
for (let i = 0; i < filteredOrder.length; i++) {
|
|
2873
|
+
const id = filteredOrder[i];
|
|
2874
|
+
out[i] = { id, data: getRowData(id) ?? {} };
|
|
2875
|
+
}
|
|
2876
|
+
return out;
|
|
2877
|
+
},
|
|
2878
|
+
setSort(column, direction, append) {
|
|
2879
|
+
if (append) {
|
|
2880
|
+
sorts = sorts.filter((s) => s.column !== column);
|
|
2881
|
+
sorts.push({ column, direction });
|
|
2882
|
+
} else {
|
|
2883
|
+
sorts = [{ column, direction }];
|
|
2884
|
+
}
|
|
2885
|
+
recomputeFiltered();
|
|
2886
|
+
},
|
|
2887
|
+
setSorts(newSorts) {
|
|
2888
|
+
sorts = [...newSorts];
|
|
2889
|
+
recomputeFiltered();
|
|
2890
|
+
},
|
|
2891
|
+
clearSort() {
|
|
2892
|
+
sorts = [];
|
|
2893
|
+
recomputeFiltered();
|
|
2894
|
+
},
|
|
2895
|
+
addFilter(filter) {
|
|
2896
|
+
filters = filters.filter((f) => f.column !== filter.column);
|
|
2897
|
+
filters.push(filter);
|
|
2898
|
+
page = 1;
|
|
2899
|
+
recomputeFiltered();
|
|
2900
|
+
},
|
|
2901
|
+
removeFilter(column) {
|
|
2902
|
+
filters = filters.filter((f) => f.column !== column);
|
|
2903
|
+
recomputeFiltered();
|
|
2904
|
+
},
|
|
2905
|
+
clearFilters() {
|
|
2906
|
+
filters = [];
|
|
2907
|
+
customFilterFn = null;
|
|
2908
|
+
recomputeFiltered();
|
|
2909
|
+
},
|
|
2910
|
+
setFilterFn(fn) {
|
|
2911
|
+
customFilterFn = fn;
|
|
2912
|
+
recomputeFiltered();
|
|
2913
|
+
},
|
|
2914
|
+
setColumnFilter(column, filter) {
|
|
2915
|
+
filters = filters.filter((f) => f.column !== column);
|
|
2916
|
+
if (filter !== null) {
|
|
2917
|
+
filters.push(toColumnFilter(column, filter));
|
|
2918
|
+
}
|
|
2919
|
+
page = 1;
|
|
2920
|
+
recomputeFiltered();
|
|
2921
|
+
},
|
|
2922
|
+
setPage(p) {
|
|
2923
|
+
const totalPages = Math.max(1, Math.ceil(filteredOrder.length / currentPageSize));
|
|
2924
|
+
page = Math.max(1, Math.min(p, totalPages));
|
|
2925
|
+
publishState();
|
|
2926
|
+
},
|
|
2927
|
+
setPageSize(size) {
|
|
2928
|
+
currentPageSize = size;
|
|
2929
|
+
page = 1;
|
|
2930
|
+
recomputeFiltered();
|
|
2931
|
+
},
|
|
2932
|
+
nextPage() {
|
|
2933
|
+
table.setPage(page + 1);
|
|
2934
|
+
},
|
|
2935
|
+
prevPage() {
|
|
2936
|
+
table.setPage(page - 1);
|
|
2937
|
+
},
|
|
2938
|
+
moveRow(fromIndex, toIndex) {
|
|
2939
|
+
if (fromIndex < 0 || fromIndex >= order.length) return;
|
|
2940
|
+
if (toIndex < 0 || toIndex >= order.length) return;
|
|
2941
|
+
if (fromIndex === toIndex) return;
|
|
2942
|
+
const [item] = order.splice(fromIndex, 1);
|
|
2943
|
+
order.splice(toIndex, 0, item);
|
|
2944
|
+
recomputeFiltered();
|
|
2945
|
+
},
|
|
2946
|
+
pinColumn(column, side) {
|
|
2947
|
+
pinnedLeft = pinnedLeft.filter((c) => c !== column);
|
|
2948
|
+
pinnedRight = pinnedRight.filter((c) => c !== column);
|
|
2949
|
+
if (side === "left") pinnedLeft.push(column);
|
|
2950
|
+
else if (side === "right") pinnedRight.push(column);
|
|
2951
|
+
core.set(statePath(tableId, "pinnedLeft"), [...pinnedLeft]);
|
|
2952
|
+
core.set(statePath(tableId, "pinnedRight"), [...pinnedRight]);
|
|
2953
|
+
},
|
|
2954
|
+
getPinnedColumns() {
|
|
2955
|
+
return { left: [...pinnedLeft], right: [...pinnedRight] };
|
|
2956
|
+
},
|
|
2957
|
+
clearPinned() {
|
|
2958
|
+
pinnedLeft = [];
|
|
2959
|
+
pinnedRight = [];
|
|
2960
|
+
core.set(statePath(tableId, "pinnedLeft"), []);
|
|
2961
|
+
core.set(statePath(tableId, "pinnedRight"), []);
|
|
2962
|
+
},
|
|
2963
|
+
setColumnWidth(column, width) {
|
|
2964
|
+
columnWidths = { ...columnWidths, [column]: width };
|
|
2965
|
+
core.set(statePath(tableId, "columnWidths"), { ...columnWidths });
|
|
2966
|
+
},
|
|
2967
|
+
getColumnWidth(column) {
|
|
2968
|
+
return columnWidths[column];
|
|
2969
|
+
},
|
|
2970
|
+
getColumnWidths() {
|
|
2971
|
+
return { ...columnWidths };
|
|
2972
|
+
},
|
|
2973
|
+
clearColumnWidths() {
|
|
2974
|
+
columnWidths = {};
|
|
2975
|
+
core.set(statePath(tableId, "columnWidths"), {});
|
|
2976
|
+
},
|
|
2977
|
+
moveRows(ids, toIndex) {
|
|
2978
|
+
const items = ids.map((id) => {
|
|
2979
|
+
const idx = order.indexOf(id);
|
|
2980
|
+
return { id, idx };
|
|
2981
|
+
}).filter((x) => x.idx !== -1);
|
|
2982
|
+
if (items.length === 0) return;
|
|
2983
|
+
const idsSet = new Set(ids);
|
|
2984
|
+
const remaining = order.filter((id) => !idsSet.has(id));
|
|
2985
|
+
const clamped = Math.max(0, Math.min(toIndex, remaining.length));
|
|
2986
|
+
remaining.splice(clamped, 0, ...items.map((x) => x.id));
|
|
2987
|
+
order = remaining;
|
|
2988
|
+
recomputeFiltered();
|
|
2989
|
+
},
|
|
2990
|
+
getExportData(exportOptions) {
|
|
2991
|
+
const cols = exportOptions?.columns ?? getColumnKeys();
|
|
2992
|
+
const result = [];
|
|
2993
|
+
if (exportOptions?.headers !== false) {
|
|
2994
|
+
result.push(cols);
|
|
2995
|
+
}
|
|
2996
|
+
const sourceIds = exportOptions?.allRows ? filteredOrder : filteredOrder.slice(
|
|
2997
|
+
(page - 1) * currentPageSize,
|
|
2998
|
+
(page - 1) * currentPageSize + currentPageSize
|
|
2999
|
+
);
|
|
3000
|
+
for (const id of sourceIds) {
|
|
3001
|
+
const data = getRowData(id);
|
|
3002
|
+
if (!data) continue;
|
|
3003
|
+
result.push(
|
|
3004
|
+
cols.map((col) => {
|
|
3005
|
+
const v = data[col];
|
|
3006
|
+
return v == null ? "" : String(v);
|
|
3007
|
+
})
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
return result;
|
|
3011
|
+
},
|
|
3012
|
+
getTableState() {
|
|
3013
|
+
const totalPages = Math.max(1, Math.ceil(filteredOrder.length / currentPageSize));
|
|
3014
|
+
return {
|
|
3015
|
+
totalRows: order.length,
|
|
3016
|
+
filteredRows: filteredOrder.length,
|
|
3017
|
+
page,
|
|
3018
|
+
pageSize: currentPageSize,
|
|
3019
|
+
totalPages,
|
|
3020
|
+
sorts: [...sorts],
|
|
3021
|
+
sort: sorts.length > 0 ? sorts[0] : null,
|
|
3022
|
+
filters: [...filters],
|
|
3023
|
+
groupBy: groupByField,
|
|
3024
|
+
groupCount: groups.length
|
|
3025
|
+
};
|
|
3026
|
+
},
|
|
3027
|
+
groupBy(field, aggregates) {
|
|
3028
|
+
groupByField = field ?? null;
|
|
3029
|
+
columnAggregates = aggregates ? [...aggregates] : [];
|
|
3030
|
+
recomputeGroups();
|
|
3031
|
+
publishState();
|
|
3032
|
+
},
|
|
3033
|
+
clearGroupBy() {
|
|
3034
|
+
groupByField = null;
|
|
3035
|
+
columnAggregates = [];
|
|
3036
|
+
groups = [];
|
|
3037
|
+
publishState();
|
|
3038
|
+
},
|
|
3039
|
+
getGroupBy() {
|
|
3040
|
+
return groupByField;
|
|
3041
|
+
},
|
|
3042
|
+
getGroups() {
|
|
3043
|
+
return groups.map((g) => ({
|
|
3044
|
+
key: g.key,
|
|
3045
|
+
value: g.value,
|
|
3046
|
+
rows: [...g.rows],
|
|
3047
|
+
aggregates: { ...g.aggregates }
|
|
3048
|
+
}));
|
|
3049
|
+
},
|
|
3050
|
+
getEstimatedRowHeight(id) {
|
|
3051
|
+
const fn = options?.estimateRowHeight;
|
|
3052
|
+
if (!fn) return null;
|
|
3053
|
+
const data = getRowData(id);
|
|
3054
|
+
if (!data) return null;
|
|
3055
|
+
const v = fn(data, id);
|
|
3056
|
+
return Number.isFinite(v) ? v : null;
|
|
3057
|
+
},
|
|
3058
|
+
reset() {
|
|
3059
|
+
order = [];
|
|
3060
|
+
filteredOrder = [];
|
|
3061
|
+
sorts = [];
|
|
3062
|
+
filters = [];
|
|
3063
|
+
customFilterFn = null;
|
|
3064
|
+
page = 1;
|
|
3065
|
+
pinnedLeft = [];
|
|
3066
|
+
pinnedRight = [];
|
|
3067
|
+
columnWidths = {};
|
|
3068
|
+
groupByField = null;
|
|
3069
|
+
columnAggregates = [];
|
|
3070
|
+
groups = [];
|
|
3071
|
+
dataById.clear();
|
|
3072
|
+
keysById.clear();
|
|
3073
|
+
core.set(statePath(tableId, "columnWidths"), {});
|
|
3074
|
+
publishState();
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
core.set(statePath(tableId, "columnWidths"), {});
|
|
3078
|
+
publishState();
|
|
3079
|
+
return table;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// src/engines/tree/TreeEngine.ts
|
|
3083
|
+
var treeCounter = 0;
|
|
3084
|
+
function createTree(core, options) {
|
|
3085
|
+
const treeId = `tr${treeCounter++}`;
|
|
3086
|
+
const fn = {
|
|
3087
|
+
key: options?.fieldNames?.key ?? "key",
|
|
3088
|
+
title: options?.fieldNames?.title ?? "title",
|
|
3089
|
+
children: options?.fieldNames?.children ?? "children"
|
|
3090
|
+
};
|
|
3091
|
+
const checkStrictly = options?.checkStrictly ?? false;
|
|
3092
|
+
let nodes = [];
|
|
3093
|
+
let nodeMap = /* @__PURE__ */ new Map();
|
|
3094
|
+
let expandedKeys = new Set(options?.defaultExpandedKeys ?? []);
|
|
3095
|
+
let checkedKeys = new Set(options?.defaultCheckedKeys ?? []);
|
|
3096
|
+
let halfCheckedKeys = /* @__PURE__ */ new Set();
|
|
3097
|
+
let selectedKeys = new Set(options?.defaultSelectedKeys ?? []);
|
|
3098
|
+
let loadedKeys = /* @__PURE__ */ new Set();
|
|
3099
|
+
let loadingKeys = /* @__PURE__ */ new Set();
|
|
3100
|
+
let filteredKeys = null;
|
|
3101
|
+
function getKey(node) {
|
|
3102
|
+
return node[fn.key];
|
|
3103
|
+
}
|
|
3104
|
+
function getChildren(node) {
|
|
3105
|
+
return node[fn.children];
|
|
3106
|
+
}
|
|
3107
|
+
function buildMap(data) {
|
|
3108
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
3109
|
+
function walk(list, parentKey, depth) {
|
|
3110
|
+
for (const n of list) {
|
|
3111
|
+
const k = getKey(n);
|
|
3112
|
+
const children = getChildren(n);
|
|
3113
|
+
const childKeys = [];
|
|
3114
|
+
const isLeaf = n.isLeaf === true || (!children || children.length === 0);
|
|
3115
|
+
if (children && children.length > 0) {
|
|
3116
|
+
for (const ch of children) childKeys.push(getKey(ch));
|
|
3117
|
+
}
|
|
3118
|
+
nodeMap.set(k, { node: n, key: k, depth, parentKey, childKeys, isLeaf });
|
|
3119
|
+
if (children && children.length > 0) {
|
|
3120
|
+
walk(children, k, depth + 1);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
walk(data, null, 0);
|
|
3125
|
+
}
|
|
3126
|
+
function getAllDescendantKeys(key2) {
|
|
3127
|
+
const result = [];
|
|
3128
|
+
const internal = nodeMap.get(key2);
|
|
3129
|
+
if (!internal) return result;
|
|
3130
|
+
for (const ck of internal.childKeys) {
|
|
3131
|
+
result.push(ck);
|
|
3132
|
+
result.push(...getAllDescendantKeys(ck));
|
|
3133
|
+
}
|
|
3134
|
+
return result;
|
|
3135
|
+
}
|
|
3136
|
+
function getAllAncestorKeys(key2) {
|
|
3137
|
+
const result = [];
|
|
3138
|
+
let current = nodeMap.get(key2);
|
|
3139
|
+
while (current && current.parentKey !== null) {
|
|
3140
|
+
result.push(current.parentKey);
|
|
3141
|
+
current = nodeMap.get(current.parentKey);
|
|
3142
|
+
}
|
|
3143
|
+
return result;
|
|
3144
|
+
}
|
|
3145
|
+
function recomputeHalfChecked() {
|
|
3146
|
+
if (checkStrictly) {
|
|
3147
|
+
halfCheckedKeys = /* @__PURE__ */ new Set();
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
const half = /* @__PURE__ */ new Set();
|
|
3151
|
+
for (const [key2, internal] of nodeMap) {
|
|
3152
|
+
if (internal.childKeys.length === 0) continue;
|
|
3153
|
+
if (checkedKeys.has(key2)) continue;
|
|
3154
|
+
const allDescendants = getAllDescendantKeys(key2);
|
|
3155
|
+
const someChecked = allDescendants.some((dk) => checkedKeys.has(dk));
|
|
3156
|
+
if (someChecked) half.add(key2);
|
|
3157
|
+
}
|
|
3158
|
+
halfCheckedKeys = half;
|
|
3159
|
+
}
|
|
3160
|
+
function cascadeCheck(key2, checked) {
|
|
3161
|
+
if (checkStrictly) {
|
|
3162
|
+
if (checked) checkedKeys.add(key2);
|
|
3163
|
+
else checkedKeys.delete(key2);
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
const internal = nodeMap.get(key2);
|
|
3167
|
+
if (!internal) return;
|
|
3168
|
+
if (checked) {
|
|
3169
|
+
checkedKeys.add(key2);
|
|
3170
|
+
const descendants = getAllDescendantKeys(key2);
|
|
3171
|
+
for (const dk of descendants) {
|
|
3172
|
+
const dn = nodeMap.get(dk);
|
|
3173
|
+
if (dn && !dn.node.disabled && !dn.node.disableCheckbox) {
|
|
3174
|
+
checkedKeys.add(dk);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
} else {
|
|
3178
|
+
checkedKeys.delete(key2);
|
|
3179
|
+
const descendants = getAllDescendantKeys(key2);
|
|
3180
|
+
for (const dk of descendants) {
|
|
3181
|
+
checkedKeys.delete(dk);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
const ancestors = getAllAncestorKeys(key2);
|
|
3185
|
+
for (const ak of ancestors) {
|
|
3186
|
+
const an = nodeMap.get(ak);
|
|
3187
|
+
if (!an) continue;
|
|
3188
|
+
const allChildren = getAllDescendantKeys(ak);
|
|
3189
|
+
const enabledChildren = allChildren.filter((ck) => {
|
|
3190
|
+
const cn = nodeMap.get(ck);
|
|
3191
|
+
return cn && !cn.node.disabled && !cn.node.disableCheckbox;
|
|
3192
|
+
});
|
|
3193
|
+
const allChecked = enabledChildren.length > 0 && enabledChildren.every((ck) => checkedKeys.has(ck));
|
|
3194
|
+
if (allChecked) {
|
|
3195
|
+
checkedKeys.add(ak);
|
|
3196
|
+
} else {
|
|
3197
|
+
checkedKeys.delete(ak);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
recomputeHalfChecked();
|
|
3201
|
+
}
|
|
3202
|
+
function removeFromTree(data, targetKey) {
|
|
3203
|
+
let removed = null;
|
|
3204
|
+
const remaining = data.filter((n) => {
|
|
3205
|
+
if (getKey(n) === targetKey) {
|
|
3206
|
+
removed = n;
|
|
3207
|
+
return false;
|
|
3208
|
+
}
|
|
3209
|
+
return true;
|
|
3210
|
+
});
|
|
3211
|
+
if (!removed) {
|
|
3212
|
+
for (const n of remaining) {
|
|
3213
|
+
const children = getChildren(n);
|
|
3214
|
+
if (children && children.length > 0) {
|
|
3215
|
+
const result = removeFromTree(children, targetKey);
|
|
3216
|
+
if (result.removed) {
|
|
3217
|
+
;
|
|
3218
|
+
n[fn.children] = result.remaining;
|
|
3219
|
+
return { remaining, removed: result.removed };
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return { remaining, removed };
|
|
3225
|
+
}
|
|
3226
|
+
function insertIntoTree(data, targetKey, node, position) {
|
|
3227
|
+
if (position === 0) {
|
|
3228
|
+
return data.map((n) => {
|
|
3229
|
+
if (getKey(n) === targetKey) {
|
|
3230
|
+
const children2 = getChildren(n) ?? [];
|
|
3231
|
+
n[fn.children] = [...children2, node];
|
|
3232
|
+
return n;
|
|
3233
|
+
}
|
|
3234
|
+
const children = getChildren(n);
|
|
3235
|
+
if (children && children.length > 0) {
|
|
3236
|
+
;
|
|
3237
|
+
n[fn.children] = insertIntoTree(children, targetKey, node, position);
|
|
3238
|
+
}
|
|
3239
|
+
return n;
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
const result = [];
|
|
3243
|
+
for (const n of data) {
|
|
3244
|
+
if (getKey(n) === targetKey) {
|
|
3245
|
+
if (position === -1) {
|
|
3246
|
+
result.push(node, n);
|
|
3247
|
+
} else {
|
|
3248
|
+
result.push(n, node);
|
|
3249
|
+
}
|
|
3250
|
+
} else {
|
|
3251
|
+
const children = getChildren(n);
|
|
3252
|
+
if (children && children.length > 0) {
|
|
3253
|
+
;
|
|
3254
|
+
n[fn.children] = insertIntoTree(children, targetKey, node, position);
|
|
3255
|
+
}
|
|
3256
|
+
result.push(n);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
return result;
|
|
3260
|
+
}
|
|
3261
|
+
function publishState() {
|
|
3262
|
+
const path = (k) => `${TREE_PREFIX}${treeId}.${k}`;
|
|
3263
|
+
core.batch(() => {
|
|
3264
|
+
core.set(path("expandedKeys"), [...expandedKeys]);
|
|
3265
|
+
core.set(path("checkedKeys"), [...checkedKeys]);
|
|
3266
|
+
core.set(path("halfCheckedKeys"), [...halfCheckedKeys]);
|
|
3267
|
+
core.set(path("selectedKeys"), [...selectedKeys]);
|
|
3268
|
+
core.set(path("loadedKeys"), [...loadedKeys]);
|
|
3269
|
+
core.set(path("loadingKeys"), [...loadingKeys]);
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
function initDefaults() {
|
|
3273
|
+
if (options?.defaultExpandAll) {
|
|
3274
|
+
for (const [key2, internal] of nodeMap) {
|
|
3275
|
+
if (!internal.isLeaf) expandedKeys.add(key2);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
if (options?.defaultCheckedKeys && !checkStrictly) {
|
|
3279
|
+
for (const key2 of options.defaultCheckedKeys) {
|
|
3280
|
+
cascadeCheck(key2, true);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
recomputeHalfChecked();
|
|
3284
|
+
publishState();
|
|
3285
|
+
}
|
|
3286
|
+
const engine = {
|
|
3287
|
+
setData(data) {
|
|
3288
|
+
nodes = data;
|
|
3289
|
+
buildMap(data);
|
|
3290
|
+
initDefaults();
|
|
3291
|
+
},
|
|
3292
|
+
getData() {
|
|
3293
|
+
return nodes;
|
|
3294
|
+
},
|
|
3295
|
+
// Expand
|
|
3296
|
+
expand(key2) {
|
|
3297
|
+
expandedKeys.add(key2);
|
|
3298
|
+
publishState();
|
|
3299
|
+
},
|
|
3300
|
+
collapse(key2) {
|
|
3301
|
+
expandedKeys.delete(key2);
|
|
3302
|
+
publishState();
|
|
3303
|
+
},
|
|
3304
|
+
toggleExpand(key2) {
|
|
3305
|
+
if (expandedKeys.has(key2)) expandedKeys.delete(key2);
|
|
3306
|
+
else expandedKeys.add(key2);
|
|
3307
|
+
publishState();
|
|
3308
|
+
},
|
|
3309
|
+
setExpandedKeys(keys) {
|
|
3310
|
+
expandedKeys = new Set(keys);
|
|
3311
|
+
publishState();
|
|
3312
|
+
},
|
|
3313
|
+
expandAll() {
|
|
3314
|
+
for (const [key2, internal] of nodeMap) {
|
|
3315
|
+
if (!internal.isLeaf) expandedKeys.add(key2);
|
|
3316
|
+
}
|
|
3317
|
+
publishState();
|
|
3318
|
+
},
|
|
3319
|
+
collapseAll() {
|
|
3320
|
+
expandedKeys = /* @__PURE__ */ new Set();
|
|
3321
|
+
publishState();
|
|
3322
|
+
},
|
|
3323
|
+
// Check
|
|
3324
|
+
check(key2) {
|
|
3325
|
+
cascadeCheck(key2, true);
|
|
3326
|
+
publishState();
|
|
3327
|
+
},
|
|
3328
|
+
uncheck(key2) {
|
|
3329
|
+
cascadeCheck(key2, false);
|
|
3330
|
+
publishState();
|
|
3331
|
+
},
|
|
3332
|
+
toggleCheck(key2) {
|
|
3333
|
+
const checked = !checkedKeys.has(key2);
|
|
3334
|
+
cascadeCheck(key2, checked);
|
|
3335
|
+
publishState();
|
|
3336
|
+
},
|
|
3337
|
+
setCheckedKeys(keys) {
|
|
3338
|
+
checkedKeys = new Set(keys);
|
|
3339
|
+
recomputeHalfChecked();
|
|
3340
|
+
publishState();
|
|
3341
|
+
},
|
|
3342
|
+
// Select
|
|
3343
|
+
select(key2, multiple) {
|
|
3344
|
+
if (multiple) {
|
|
3345
|
+
if (selectedKeys.has(key2)) selectedKeys.delete(key2);
|
|
3346
|
+
else selectedKeys.add(key2);
|
|
3347
|
+
} else {
|
|
3348
|
+
selectedKeys = /* @__PURE__ */ new Set([key2]);
|
|
3349
|
+
}
|
|
3350
|
+
publishState();
|
|
3351
|
+
},
|
|
3352
|
+
deselect(key2) {
|
|
3353
|
+
selectedKeys.delete(key2);
|
|
3354
|
+
publishState();
|
|
3355
|
+
},
|
|
3356
|
+
setSelectedKeys(keys) {
|
|
3357
|
+
selectedKeys = new Set(keys);
|
|
3358
|
+
publishState();
|
|
3359
|
+
},
|
|
3360
|
+
// Async loading
|
|
3361
|
+
markLoading(key2) {
|
|
3362
|
+
loadingKeys.add(key2);
|
|
3363
|
+
publishState();
|
|
3364
|
+
},
|
|
3365
|
+
markLoaded(key2) {
|
|
3366
|
+
loadingKeys.delete(key2);
|
|
3367
|
+
loadedKeys.add(key2);
|
|
3368
|
+
publishState();
|
|
3369
|
+
},
|
|
3370
|
+
addChildren(parentKey, children) {
|
|
3371
|
+
const internal = nodeMap.get(parentKey);
|
|
3372
|
+
if (!internal) return;
|
|
3373
|
+
internal.node[fn.children] = children;
|
|
3374
|
+
buildMap(nodes);
|
|
3375
|
+
publishState();
|
|
3376
|
+
},
|
|
3377
|
+
// Drag & drop
|
|
3378
|
+
moveNode(dragKey, dropKey, position) {
|
|
3379
|
+
if (dragKey === dropKey) return null;
|
|
3380
|
+
const ancestors = getAllAncestorKeys(dropKey);
|
|
3381
|
+
if (ancestors.includes(dragKey)) return null;
|
|
3382
|
+
const { remaining, removed } = removeFromTree(nodes, dragKey);
|
|
3383
|
+
if (!removed) return null;
|
|
3384
|
+
nodes = insertIntoTree(remaining, dropKey, removed, position);
|
|
3385
|
+
buildMap(nodes);
|
|
3386
|
+
publishState();
|
|
3387
|
+
return nodes;
|
|
3388
|
+
},
|
|
3389
|
+
// Filter
|
|
3390
|
+
filterNodes(predicate) {
|
|
3391
|
+
if (!predicate) {
|
|
3392
|
+
filteredKeys = null;
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
const matched = /* @__PURE__ */ new Set();
|
|
3396
|
+
for (const [key2, internal] of nodeMap) {
|
|
3397
|
+
if (predicate(internal.node)) {
|
|
3398
|
+
matched.add(key2);
|
|
3399
|
+
const ancestors = getAllAncestorKeys(key2);
|
|
3400
|
+
for (const ak of ancestors) matched.add(ak);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
filteredKeys = matched;
|
|
3404
|
+
},
|
|
3405
|
+
getFilteredKeys() {
|
|
3406
|
+
return filteredKeys ? [...filteredKeys] : null;
|
|
3407
|
+
},
|
|
3408
|
+
// Query
|
|
3409
|
+
getNode(key2) {
|
|
3410
|
+
return nodeMap.get(key2)?.node;
|
|
3411
|
+
},
|
|
3412
|
+
getParentKey(key2) {
|
|
3413
|
+
return nodeMap.get(key2)?.parentKey ?? null;
|
|
3414
|
+
},
|
|
3415
|
+
getChildKeys(key2) {
|
|
3416
|
+
return nodeMap.get(key2)?.childKeys ?? [];
|
|
3417
|
+
},
|
|
3418
|
+
getAllKeys() {
|
|
3419
|
+
return [...nodeMap.keys()];
|
|
3420
|
+
},
|
|
3421
|
+
getLeafKeys() {
|
|
3422
|
+
const result = [];
|
|
3423
|
+
for (const [key2, internal] of nodeMap) {
|
|
3424
|
+
if (internal.isLeaf) result.push(key2);
|
|
3425
|
+
}
|
|
3426
|
+
return result;
|
|
3427
|
+
},
|
|
3428
|
+
getState() {
|
|
3429
|
+
return {
|
|
3430
|
+
expandedKeys: [...expandedKeys],
|
|
3431
|
+
checkedKeys: [...checkedKeys],
|
|
3432
|
+
halfCheckedKeys: [...halfCheckedKeys],
|
|
3433
|
+
selectedKeys: [...selectedKeys],
|
|
3434
|
+
loadedKeys: [...loadedKeys],
|
|
3435
|
+
loadingKeys: [...loadingKeys]
|
|
3436
|
+
};
|
|
3437
|
+
},
|
|
3438
|
+
getFlatNodes() {
|
|
3439
|
+
const result = [];
|
|
3440
|
+
function walk(list, parentKey, depth) {
|
|
3441
|
+
for (const n of list) {
|
|
3442
|
+
const k = getKey(n);
|
|
3443
|
+
if (filteredKeys && !filteredKeys.has(k)) continue;
|
|
3444
|
+
const children = getChildren(n);
|
|
3445
|
+
const isLeaf = n.isLeaf === true || (!children || children.length === 0);
|
|
3446
|
+
result.push({ node: n, key: k, depth, isLeaf, parentKey });
|
|
3447
|
+
if (!isLeaf && expandedKeys.has(k) && children && children.length > 0) {
|
|
3448
|
+
walk(children, k, depth + 1);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
walk(nodes, null, 0);
|
|
3453
|
+
return result;
|
|
3454
|
+
},
|
|
3455
|
+
reset() {
|
|
3456
|
+
nodes = [];
|
|
3457
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
3458
|
+
expandedKeys = /* @__PURE__ */ new Set();
|
|
3459
|
+
checkedKeys = /* @__PURE__ */ new Set();
|
|
3460
|
+
halfCheckedKeys = /* @__PURE__ */ new Set();
|
|
3461
|
+
selectedKeys = /* @__PURE__ */ new Set();
|
|
3462
|
+
loadedKeys = /* @__PURE__ */ new Set();
|
|
3463
|
+
loadingKeys = /* @__PURE__ */ new Set();
|
|
3464
|
+
filteredKeys = null;
|
|
3465
|
+
publishState();
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
return engine;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// src/engines/calendar/availability.ts
|
|
3472
|
+
var DAY_MS = 864e5;
|
|
3473
|
+
function parseHHMM(s, fallback) {
|
|
3474
|
+
if (s === void 0) return fallback;
|
|
3475
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(s);
|
|
3476
|
+
if (!m) return fallback;
|
|
3477
|
+
const h = Number(m[1]);
|
|
3478
|
+
const mm = Number(m[2]);
|
|
3479
|
+
if (!Number.isFinite(h) || !Number.isFinite(mm)) return fallback;
|
|
3480
|
+
if (h < 0 || h > 24 || mm < 0 || mm >= 60) return fallback;
|
|
3481
|
+
return h * 36e5 + mm * 6e4;
|
|
3482
|
+
}
|
|
3483
|
+
function rulesFor(resourceId, rules) {
|
|
3484
|
+
return rules.filter((r) => r.resourceId === resourceId);
|
|
3485
|
+
}
|
|
3486
|
+
function dayBucket(t) {
|
|
3487
|
+
const d = new Date(t);
|
|
3488
|
+
const dayStart = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
3489
|
+
const dayOfWeek = new Date(dayStart).getUTCDay();
|
|
3490
|
+
return { dayStart, dayOfWeek };
|
|
3491
|
+
}
|
|
3492
|
+
function isAvailable(resource, range, rules) {
|
|
3493
|
+
if (range.end <= range.start) return true;
|
|
3494
|
+
const own = rulesFor(resource.id, rules);
|
|
3495
|
+
if (own.length === 0) return true;
|
|
3496
|
+
let cursor = range.start;
|
|
3497
|
+
while (cursor < range.end) {
|
|
3498
|
+
const { dayStart, dayOfWeek } = dayBucket(cursor);
|
|
3499
|
+
const dayEnd = dayStart + DAY_MS;
|
|
3500
|
+
const sliceEnd = Math.min(range.end, dayEnd);
|
|
3501
|
+
const dayRules = own.filter(
|
|
3502
|
+
(r) => r.dayOfWeek === void 0 || r.dayOfWeek === dayOfWeek
|
|
3503
|
+
);
|
|
3504
|
+
if (dayRules.length === 0) return false;
|
|
3505
|
+
const intervals = dayRules.map((r) => {
|
|
3506
|
+
const fromMs = parseHHMM(r.from, 0);
|
|
3507
|
+
const toMs = parseHHMM(r.to, DAY_MS);
|
|
3508
|
+
return { from: dayStart + fromMs, to: dayStart + toMs };
|
|
3509
|
+
}).filter((w) => w.to > w.from).sort((a, b) => a.from - b.from);
|
|
3510
|
+
if (intervals.length === 0) return false;
|
|
3511
|
+
const merged = [];
|
|
3512
|
+
for (const w of intervals) {
|
|
3513
|
+
const last = merged[merged.length - 1];
|
|
3514
|
+
if (last && w.from <= last.to) {
|
|
3515
|
+
last.to = Math.max(last.to, w.to);
|
|
3516
|
+
} else {
|
|
3517
|
+
merged.push({ ...w });
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
let needed = cursor;
|
|
3521
|
+
for (const w of merged) {
|
|
3522
|
+
if (w.from > needed) return false;
|
|
3523
|
+
if (w.to >= sliceEnd) {
|
|
3524
|
+
needed = sliceEnd;
|
|
3525
|
+
break;
|
|
3526
|
+
}
|
|
3527
|
+
needed = Math.max(needed, w.to);
|
|
3528
|
+
}
|
|
3529
|
+
if (needed < sliceEnd) return false;
|
|
3530
|
+
cursor = sliceEnd;
|
|
3531
|
+
}
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
// src/engines/calendar/conflicts.ts
|
|
3536
|
+
var DAY_MS2 = 864e5;
|
|
3537
|
+
var STEP_MS = {
|
|
3538
|
+
day: DAY_MS2,
|
|
3539
|
+
week: 7 * DAY_MS2,
|
|
3540
|
+
month: 30 * DAY_MS2
|
|
3541
|
+
};
|
|
3542
|
+
function makeId(reason, ids) {
|
|
3543
|
+
return `${reason}:${[...ids].sort().join(",")}`;
|
|
3544
|
+
}
|
|
3545
|
+
function pushConflict(out, reason, ids) {
|
|
3546
|
+
if (ids.length === 0) return;
|
|
3547
|
+
const sorted = [...ids].sort();
|
|
3548
|
+
const id = makeId(reason, sorted);
|
|
3549
|
+
if (out.has(id)) return;
|
|
3550
|
+
out.set(id, { id, reason, assignmentIds: sorted });
|
|
3551
|
+
}
|
|
3552
|
+
function detectConflicts(state, opts = {}) {
|
|
3553
|
+
const scale = opts.scale ?? "day";
|
|
3554
|
+
const step = STEP_MS[scale];
|
|
3555
|
+
const rules = opts.rules ?? [];
|
|
3556
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
3557
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3558
|
+
for (const a of state.assignments) {
|
|
3559
|
+
let bucket = byResource.get(a.resourceId);
|
|
3560
|
+
if (!bucket) {
|
|
3561
|
+
bucket = [];
|
|
3562
|
+
byResource.set(a.resourceId, bucket);
|
|
3563
|
+
}
|
|
3564
|
+
bucket.push(a);
|
|
3565
|
+
}
|
|
3566
|
+
for (const list of byResource.values()) {
|
|
3567
|
+
const sorted = [...list].sort((x, y) => x.start - y.start);
|
|
3568
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
3569
|
+
const a = sorted[i];
|
|
3570
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
3571
|
+
const b = sorted[j];
|
|
3572
|
+
if (b.start >= a.end) break;
|
|
3573
|
+
pushConflict(conflicts, "overlap", [a.id, b.id]);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const resourceById = /* @__PURE__ */ new Map();
|
|
3578
|
+
for (const r of state.resources) resourceById.set(r.id, r);
|
|
3579
|
+
for (const [rid, list] of byResource) {
|
|
3580
|
+
const resource = resourceById.get(rid);
|
|
3581
|
+
if (!resource || resource.capacityPerSlot === void 0) continue;
|
|
3582
|
+
const cap = Math.max(0, Math.floor(resource.capacityPerSlot));
|
|
3583
|
+
const slotMap = /* @__PURE__ */ new Map();
|
|
3584
|
+
for (const a of list) {
|
|
3585
|
+
const lo = Math.floor(a.start / step) * step;
|
|
3586
|
+
const hi = Math.ceil(a.end / step) * step;
|
|
3587
|
+
for (let s = lo; s < hi; s += step) {
|
|
3588
|
+
let bucket = slotMap.get(s);
|
|
3589
|
+
if (!bucket) {
|
|
3590
|
+
bucket = [];
|
|
3591
|
+
slotMap.set(s, bucket);
|
|
3592
|
+
}
|
|
3593
|
+
bucket.push(a.id);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
for (const ids of slotMap.values()) {
|
|
3597
|
+
if (ids.length > cap) {
|
|
3598
|
+
pushConflict(conflicts, "over-capacity", ids);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (rules.length > 0) {
|
|
3603
|
+
for (const a of state.assignments) {
|
|
3604
|
+
const r = resourceById.get(a.resourceId);
|
|
3605
|
+
if (!r) continue;
|
|
3606
|
+
if (!isAvailable(r, { start: a.start, end: a.end }, rules)) {
|
|
3607
|
+
pushConflict(conflicts, "outside-availability", [a.id]);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
return [...conflicts.values()];
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// src/engines/calendar/CalendarEngine.ts
|
|
3615
|
+
var calendarCounter = 0;
|
|
3616
|
+
function createCalendar(core, options) {
|
|
3617
|
+
const engineId = `c${calendarCounter++}${options?.name ? `-${options.name}` : ""}`;
|
|
3618
|
+
const prefix = CALENDAR_PREFIX;
|
|
3619
|
+
const resources = /* @__PURE__ */ new Map();
|
|
3620
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3621
|
+
let rules = options?.rules ? [...options.rules] : [];
|
|
3622
|
+
let scale = options?.scale ?? "day";
|
|
3623
|
+
let conflicts = [];
|
|
3624
|
+
const localSubscribers = /* @__PURE__ */ new Set();
|
|
3625
|
+
function publish() {
|
|
3626
|
+
const resList = [...resources.values()];
|
|
3627
|
+
const assignList = [...assignments.values()];
|
|
3628
|
+
core.batch(() => {
|
|
3629
|
+
core.set(`${prefix}${engineId}.resources`, resList);
|
|
3630
|
+
core.set(`${prefix}${engineId}.assignments`, assignList);
|
|
3631
|
+
core.set(`${prefix}${engineId}.conflicts`, conflicts);
|
|
3632
|
+
core.set(`${prefix}${engineId}.view`, { scale });
|
|
3633
|
+
});
|
|
3634
|
+
for (const cb of localSubscribers) cb();
|
|
3635
|
+
}
|
|
3636
|
+
function recompute() {
|
|
3637
|
+
conflicts = detectConflicts(
|
|
3638
|
+
{ resources: [...resources.values()], assignments: [...assignments.values()] },
|
|
3639
|
+
{ scale, rules }
|
|
3640
|
+
);
|
|
3641
|
+
const involved = /* @__PURE__ */ new Set();
|
|
3642
|
+
for (const c of conflicts) for (const id of c.assignmentIds) involved.add(id);
|
|
3643
|
+
for (const [id, a] of assignments) {
|
|
3644
|
+
const isConflicting = involved.has(id);
|
|
3645
|
+
if (isConflicting && a.status !== "conflict") {
|
|
3646
|
+
assignments.set(id, { ...a, status: "conflict" });
|
|
3647
|
+
} else if (!isConflicting && a.status === "conflict") {
|
|
3648
|
+
assignments.set(id, { ...a, status: "tentative" });
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
function commitAndPublish() {
|
|
3653
|
+
recompute();
|
|
3654
|
+
publish();
|
|
3655
|
+
}
|
|
3656
|
+
function subscribe(cb) {
|
|
3657
|
+
localSubscribers.add(cb);
|
|
3658
|
+
return () => {
|
|
3659
|
+
localSubscribers.delete(cb);
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
function addResource(resource) {
|
|
3663
|
+
if (resources.has(resource.id)) {
|
|
3664
|
+
throw new Error(
|
|
3665
|
+
`[skygraph/calendar] resource id "${resource.id}" already exists`
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
resources.set(resource.id, { ...resource });
|
|
3669
|
+
commitAndPublish();
|
|
3670
|
+
}
|
|
3671
|
+
function removeResource(id) {
|
|
3672
|
+
if (!resources.has(id)) return;
|
|
3673
|
+
resources.delete(id);
|
|
3674
|
+
for (const [aid, a] of assignments) {
|
|
3675
|
+
if (a.resourceId === id) assignments.delete(aid);
|
|
3676
|
+
}
|
|
3677
|
+
commitAndPublish();
|
|
3678
|
+
}
|
|
3679
|
+
function updateResource(id, patch) {
|
|
3680
|
+
const r = resources.get(id);
|
|
3681
|
+
if (!r) throw new Error(`[skygraph/calendar] resource "${id}" not found`);
|
|
3682
|
+
resources.set(id, { ...r, ...patch, id });
|
|
3683
|
+
commitAndPublish();
|
|
3684
|
+
}
|
|
3685
|
+
function getResource(id) {
|
|
3686
|
+
return resources.get(id);
|
|
3687
|
+
}
|
|
3688
|
+
function getResources() {
|
|
3689
|
+
return [...resources.values()];
|
|
3690
|
+
}
|
|
3691
|
+
function addAssignment(assignment) {
|
|
3692
|
+
if (assignments.has(assignment.id)) {
|
|
3693
|
+
throw new Error(
|
|
3694
|
+
`[skygraph/calendar] assignment id "${assignment.id}" already exists`
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
if (!resources.has(assignment.resourceId)) {
|
|
3698
|
+
throw new Error(
|
|
3699
|
+
`[skygraph/calendar] addAssignment: resource "${assignment.resourceId}" not found`
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
if (assignment.end <= assignment.start) {
|
|
3703
|
+
throw new Error(
|
|
3704
|
+
`[skygraph/calendar] addAssignment: end (${assignment.end}) must be greater than start (${assignment.start})`
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
assignments.set(assignment.id, { ...assignment });
|
|
3708
|
+
commitAndPublish();
|
|
3709
|
+
}
|
|
3710
|
+
function updateAssignment(id, patch) {
|
|
3711
|
+
const a = assignments.get(id);
|
|
3712
|
+
if (!a) throw new Error(`[skygraph/calendar] assignment "${id}" not found`);
|
|
3713
|
+
const next = { ...a, ...patch, id };
|
|
3714
|
+
if (next.end <= next.start) {
|
|
3715
|
+
throw new Error(
|
|
3716
|
+
`[skygraph/calendar] updateAssignment: end (${next.end}) must be greater than start (${next.start})`
|
|
3717
|
+
);
|
|
3718
|
+
}
|
|
3719
|
+
if (!resources.has(next.resourceId)) {
|
|
3720
|
+
throw new Error(
|
|
3721
|
+
`[skygraph/calendar] updateAssignment: resource "${next.resourceId}" not found`
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3724
|
+
assignments.set(id, next);
|
|
3725
|
+
commitAndPublish();
|
|
3726
|
+
}
|
|
3727
|
+
function removeAssignment(id) {
|
|
3728
|
+
if (!assignments.has(id)) return;
|
|
3729
|
+
assignments.delete(id);
|
|
3730
|
+
commitAndPublish();
|
|
3731
|
+
}
|
|
3732
|
+
function moveAssignment(id, deltaMs) {
|
|
3733
|
+
const a = assignments.get(id);
|
|
3734
|
+
if (!a) throw new Error(`[skygraph/calendar] assignment "${id}" not found`);
|
|
3735
|
+
if (deltaMs === 0) return;
|
|
3736
|
+
assignments.set(id, { ...a, start: a.start + deltaMs, end: a.end + deltaMs });
|
|
3737
|
+
commitAndPublish();
|
|
3738
|
+
}
|
|
3739
|
+
function resizeAssignment(id, side, deltaMs) {
|
|
3740
|
+
const a = assignments.get(id);
|
|
3741
|
+
if (!a) throw new Error(`[skygraph/calendar] assignment "${id}" not found`);
|
|
3742
|
+
if (deltaMs === 0) return;
|
|
3743
|
+
let next;
|
|
3744
|
+
if (side === "end") {
|
|
3745
|
+
const nextEnd = Math.max(a.start + 1, a.end + deltaMs);
|
|
3746
|
+
next = { ...a, end: nextEnd };
|
|
3747
|
+
} else {
|
|
3748
|
+
const nextStart = Math.min(a.end - 1, a.start + deltaMs);
|
|
3749
|
+
next = { ...a, start: nextStart };
|
|
3750
|
+
}
|
|
3751
|
+
assignments.set(id, next);
|
|
3752
|
+
commitAndPublish();
|
|
3753
|
+
}
|
|
3754
|
+
function getAssignment(id) {
|
|
3755
|
+
return assignments.get(id);
|
|
3756
|
+
}
|
|
3757
|
+
function getAssignments() {
|
|
3758
|
+
return [...assignments.values()];
|
|
3759
|
+
}
|
|
3760
|
+
function setRules(next) {
|
|
3761
|
+
rules = [...next];
|
|
3762
|
+
commitAndPublish();
|
|
3763
|
+
}
|
|
3764
|
+
function getRules() {
|
|
3765
|
+
return [...rules];
|
|
3766
|
+
}
|
|
3767
|
+
function getConflicts() {
|
|
3768
|
+
return [...conflicts];
|
|
3769
|
+
}
|
|
3770
|
+
function setScale(next) {
|
|
3771
|
+
if (next === scale) return;
|
|
3772
|
+
scale = next;
|
|
3773
|
+
commitAndPublish();
|
|
3774
|
+
}
|
|
3775
|
+
function getScale() {
|
|
3776
|
+
return scale;
|
|
3777
|
+
}
|
|
3778
|
+
function getState() {
|
|
3779
|
+
return {
|
|
3780
|
+
resources: [...resources.values()],
|
|
3781
|
+
assignments: [...assignments.values()],
|
|
3782
|
+
conflicts: [...conflicts]
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
function clear() {
|
|
3786
|
+
resources.clear();
|
|
3787
|
+
assignments.clear();
|
|
3788
|
+
rules = [];
|
|
3789
|
+
conflicts = [];
|
|
3790
|
+
publish();
|
|
3791
|
+
}
|
|
3792
|
+
publish();
|
|
3793
|
+
return {
|
|
3794
|
+
id: engineId,
|
|
3795
|
+
addResource,
|
|
3796
|
+
removeResource,
|
|
3797
|
+
updateResource,
|
|
3798
|
+
getResource,
|
|
3799
|
+
getResources,
|
|
3800
|
+
addAssignment,
|
|
3801
|
+
updateAssignment,
|
|
3802
|
+
removeAssignment,
|
|
3803
|
+
moveAssignment,
|
|
3804
|
+
resizeAssignment,
|
|
3805
|
+
getAssignment,
|
|
3806
|
+
getAssignments,
|
|
3807
|
+
setRules,
|
|
3808
|
+
getRules,
|
|
3809
|
+
getConflicts,
|
|
3810
|
+
setScale,
|
|
3811
|
+
getScale,
|
|
3812
|
+
getState,
|
|
3813
|
+
subscribe,
|
|
3814
|
+
clear
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3818
|
+
0 && (module.exports = {
|
|
3819
|
+
CALENDAR_PREFIX,
|
|
3820
|
+
FORM_META_PREFIX,
|
|
3821
|
+
GRAPH_PREFIX,
|
|
3822
|
+
TABLE_PREFIX,
|
|
3823
|
+
TREE_PREFIX,
|
|
3824
|
+
aabbFromOBB,
|
|
3825
|
+
computeAggregate,
|
|
3826
|
+
createCalendar,
|
|
3827
|
+
createCore,
|
|
3828
|
+
createForm,
|
|
3829
|
+
createGraph,
|
|
3830
|
+
createHistory,
|
|
3831
|
+
createMeasureCache,
|
|
3832
|
+
createTable,
|
|
3833
|
+
createTree,
|
|
3834
|
+
createTypedCore,
|
|
3835
|
+
createVirtual,
|
|
3836
|
+
detectConflicts,
|
|
3837
|
+
floatingAnchor,
|
|
3838
|
+
freezeMiddleware,
|
|
3839
|
+
getBezierPath,
|
|
3840
|
+
getEdgePosition,
|
|
3841
|
+
getNodeIntersection,
|
|
3842
|
+
inferSide,
|
|
3843
|
+
isAvailable,
|
|
3844
|
+
listReservedPrefixes,
|
|
3845
|
+
loggerMiddleware,
|
|
3846
|
+
matchesColumnFilter,
|
|
3847
|
+
matchesOperator,
|
|
3848
|
+
nearestSide,
|
|
3849
|
+
obbContainsPoint,
|
|
3850
|
+
obbCorners,
|
|
3851
|
+
persistenceMiddleware,
|
|
3852
|
+
pointsToPath,
|
|
3853
|
+
pointsToRoundedPath,
|
|
3854
|
+
reservePrefix,
|
|
3855
|
+
resolveEdgeEndpoint,
|
|
3856
|
+
resolveOperator,
|
|
3857
|
+
routeOrthogonal,
|
|
3858
|
+
validationMiddleware
|
|
3859
|
+
});
|
|
3860
|
+
//# sourceMappingURL=index.cjs.map
|