@vibevibes/sdk 0.1.0 → 0.2.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 +263 -0
- package/dist/index.cjs +4893 -46
- package/dist/index.d.cts +1284 -30
- package/dist/index.d.ts +1284 -30
- package/dist/index.js +4826 -44
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -29,19 +29,87 @@ function quickTool(name, description, input_schema, handler) {
|
|
|
29
29
|
handler
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
function undoTool(zod) {
|
|
33
|
+
return {
|
|
34
|
+
name: "_state.restore",
|
|
35
|
+
description: "Restore shared state to a previous snapshot (used by undo/redo)",
|
|
36
|
+
input_schema: zod.object({
|
|
37
|
+
state: zod.record(zod.any()).describe("The state snapshot to restore")
|
|
38
|
+
}),
|
|
39
|
+
risk: "low",
|
|
40
|
+
capabilities_required: ["state.write"],
|
|
41
|
+
handler: async (ctx, input) => {
|
|
42
|
+
ctx.setState(input.state);
|
|
43
|
+
return { restored: true };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function phaseTool(zod, validPhases) {
|
|
48
|
+
const phaseSchema = validPhases ? zod.enum(validPhases) : zod.string();
|
|
49
|
+
return {
|
|
50
|
+
name: "_phase.set",
|
|
51
|
+
description: "Transition to a new phase/stage of the experience",
|
|
52
|
+
input_schema: zod.object({
|
|
53
|
+
phase: phaseSchema.describe("The phase to transition to")
|
|
54
|
+
}),
|
|
55
|
+
risk: "low",
|
|
56
|
+
capabilities_required: ["state.write"],
|
|
57
|
+
handler: async (ctx, input) => {
|
|
58
|
+
ctx.setState({ ...ctx.state, phase: input.phase });
|
|
59
|
+
return { phase: input.phase };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function defineRoomConfig(config) {
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
function defineStream(config) {
|
|
67
|
+
return {
|
|
68
|
+
name: config.name,
|
|
69
|
+
description: config.description,
|
|
70
|
+
input_schema: config.input_schema,
|
|
71
|
+
merge: config.merge,
|
|
72
|
+
rateLimit: config.rateLimit
|
|
73
|
+
};
|
|
74
|
+
}
|
|
32
75
|
function defineExperience(module) {
|
|
33
|
-
const m = module.manifest;
|
|
76
|
+
const m = module.manifest ?? {};
|
|
77
|
+
let initialState = module.initialState;
|
|
78
|
+
if (module.stateSchema && !initialState) {
|
|
79
|
+
try {
|
|
80
|
+
initialState = module.stateSchema.parse(void 0);
|
|
81
|
+
} catch {
|
|
82
|
+
try {
|
|
83
|
+
initialState = module.stateSchema.parse({});
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (module.stateSchema && initialState) {
|
|
89
|
+
try {
|
|
90
|
+
module.stateSchema.parse(initialState);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.warn(
|
|
93
|
+
`[vibevibes] initialState does not match stateSchema: ${err.message ?? err}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const agentSlots = m.agentSlots ?? module.agents;
|
|
34
98
|
return {
|
|
35
99
|
...module,
|
|
100
|
+
initialState,
|
|
36
101
|
manifest: {
|
|
37
102
|
...m,
|
|
103
|
+
title: m.title || module.name || m.id,
|
|
38
104
|
version: m.version || "0.0.1",
|
|
39
|
-
requested_capabilities: m.requested_capabilities || ["state.write"]
|
|
105
|
+
requested_capabilities: m.requested_capabilities || ["state.write"],
|
|
106
|
+
agentSlots: agentSlots ?? m.agentSlots
|
|
40
107
|
}
|
|
41
108
|
};
|
|
42
109
|
}
|
|
43
110
|
function validateExperience(module) {
|
|
44
111
|
const errors = [];
|
|
112
|
+
const warnings = [];
|
|
45
113
|
if (!module.manifest?.id) {
|
|
46
114
|
errors.push("manifest.id is required");
|
|
47
115
|
}
|
|
@@ -69,9 +137,25 @@ function validateExperience(module) {
|
|
|
69
137
|
}
|
|
70
138
|
});
|
|
71
139
|
}
|
|
140
|
+
if (module.stateSchema) {
|
|
141
|
+
if (!module.initialState) {
|
|
142
|
+
try {
|
|
143
|
+
module.stateSchema.parse({});
|
|
144
|
+
} catch {
|
|
145
|
+
warnings.push("stateSchema has required fields without defaults \u2014 provide initialState explicitly");
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
try {
|
|
149
|
+
module.stateSchema.parse(module.initialState);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
errors.push(`initialState does not match stateSchema: ${err.message ?? err}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
72
155
|
return {
|
|
73
156
|
valid: errors.length === 0,
|
|
74
|
-
errors
|
|
157
|
+
errors,
|
|
158
|
+
warnings
|
|
75
159
|
};
|
|
76
160
|
}
|
|
77
161
|
|
|
@@ -271,6 +355,198 @@ function useTypingIndicator(actorId, ephemeralState, setEphemeral, timeoutMs = 3
|
|
|
271
355
|
const typingUsers = Object.entries(ephemeralState).filter(([id, data]) => id !== actorId && data._typing && now - data._typing < timeoutMs + 2e3).map(([id]) => id);
|
|
272
356
|
return { setTyping, typingUsers };
|
|
273
357
|
}
|
|
358
|
+
function useUndo(sharedState, callTool, opts) {
|
|
359
|
+
const maxHistory = opts?.maxHistory ?? 50;
|
|
360
|
+
const restoreTool = opts?.restoreTool ?? "_state.restore";
|
|
361
|
+
const undoStackRef = React.useRef([]);
|
|
362
|
+
const redoStackRef = React.useRef([]);
|
|
363
|
+
const lastStateRef = React.useRef(null);
|
|
364
|
+
const restoringRef = React.useRef(false);
|
|
365
|
+
const [, forceRender] = React.useState(0);
|
|
366
|
+
React.useEffect(() => {
|
|
367
|
+
if (restoringRef.current) {
|
|
368
|
+
restoringRef.current = false;
|
|
369
|
+
lastStateRef.current = sharedState;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (lastStateRef.current !== null && lastStateRef.current !== sharedState) {
|
|
373
|
+
undoStackRef.current = [
|
|
374
|
+
...undoStackRef.current.slice(-(maxHistory - 1)),
|
|
375
|
+
lastStateRef.current
|
|
376
|
+
];
|
|
377
|
+
redoStackRef.current = [];
|
|
378
|
+
forceRender((n) => n + 1);
|
|
379
|
+
}
|
|
380
|
+
lastStateRef.current = sharedState;
|
|
381
|
+
}, [sharedState, maxHistory]);
|
|
382
|
+
const undo = React.useCallback(() => {
|
|
383
|
+
const stack = undoStackRef.current;
|
|
384
|
+
if (stack.length === 0) return;
|
|
385
|
+
const prev = stack[stack.length - 1];
|
|
386
|
+
undoStackRef.current = stack.slice(0, -1);
|
|
387
|
+
redoStackRef.current = [...redoStackRef.current, sharedState];
|
|
388
|
+
restoringRef.current = true;
|
|
389
|
+
forceRender((n) => n + 1);
|
|
390
|
+
callTool(restoreTool, { state: prev }).catch(() => {
|
|
391
|
+
});
|
|
392
|
+
}, [sharedState, callTool, restoreTool]);
|
|
393
|
+
const redo = React.useCallback(() => {
|
|
394
|
+
const stack = redoStackRef.current;
|
|
395
|
+
if (stack.length === 0) return;
|
|
396
|
+
const next = stack[stack.length - 1];
|
|
397
|
+
redoStackRef.current = stack.slice(0, -1);
|
|
398
|
+
undoStackRef.current = [...undoStackRef.current, sharedState];
|
|
399
|
+
restoringRef.current = true;
|
|
400
|
+
forceRender((n) => n + 1);
|
|
401
|
+
callTool(restoreTool, { state: next }).catch(() => {
|
|
402
|
+
});
|
|
403
|
+
}, [sharedState, callTool, restoreTool]);
|
|
404
|
+
return {
|
|
405
|
+
undo,
|
|
406
|
+
redo,
|
|
407
|
+
canUndo: undoStackRef.current.length > 0,
|
|
408
|
+
canRedo: redoStackRef.current.length > 0,
|
|
409
|
+
undoCount: undoStackRef.current.length,
|
|
410
|
+
redoCount: redoStackRef.current.length
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function useDebounce(callTool, delayMs = 300) {
|
|
414
|
+
const timerRef = React.useRef(null);
|
|
415
|
+
const latestRef = React.useRef(null);
|
|
416
|
+
React.useEffect(() => {
|
|
417
|
+
return () => {
|
|
418
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
419
|
+
};
|
|
420
|
+
}, []);
|
|
421
|
+
return React.useCallback(
|
|
422
|
+
(name, input) => {
|
|
423
|
+
latestRef.current = { name, input };
|
|
424
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
timerRef.current = setTimeout(async () => {
|
|
427
|
+
const latest = latestRef.current;
|
|
428
|
+
if (!latest) return;
|
|
429
|
+
try {
|
|
430
|
+
const result = await callTool(latest.name, latest.input);
|
|
431
|
+
resolve(result);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
reject(err);
|
|
434
|
+
}
|
|
435
|
+
}, delayMs);
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
[callTool, delayMs]
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
function useThrottle(callTool, intervalMs = 50) {
|
|
442
|
+
const lastCallRef = React.useRef(0);
|
|
443
|
+
const timerRef = React.useRef(null);
|
|
444
|
+
const latestRef = React.useRef(null);
|
|
445
|
+
React.useEffect(() => {
|
|
446
|
+
return () => {
|
|
447
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
448
|
+
};
|
|
449
|
+
}, []);
|
|
450
|
+
return React.useCallback(
|
|
451
|
+
(name, input) => {
|
|
452
|
+
latestRef.current = { name, input };
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
const elapsed = now - lastCallRef.current;
|
|
455
|
+
if (elapsed >= intervalMs) {
|
|
456
|
+
lastCallRef.current = now;
|
|
457
|
+
return callTool(name, input);
|
|
458
|
+
}
|
|
459
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
timerRef.current = setTimeout(async () => {
|
|
462
|
+
lastCallRef.current = Date.now();
|
|
463
|
+
const latest = latestRef.current;
|
|
464
|
+
if (!latest) return;
|
|
465
|
+
try {
|
|
466
|
+
const result = await callTool(latest.name, latest.input);
|
|
467
|
+
resolve(result);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
reject(err);
|
|
470
|
+
}
|
|
471
|
+
}, intervalMs - elapsed);
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
[callTool, intervalMs]
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
function usePhase(sharedState, callTool, config) {
|
|
478
|
+
const { phases, stateKey = "phase", toolName = "_phase.set" } = config;
|
|
479
|
+
const current = sharedState[stateKey] ?? phases[0];
|
|
480
|
+
const index = phases.indexOf(current);
|
|
481
|
+
const safeIndex = index === -1 ? 0 : index;
|
|
482
|
+
const next = React.useCallback(() => {
|
|
483
|
+
if (safeIndex < phases.length - 1) {
|
|
484
|
+
callTool(toolName, { phase: phases[safeIndex + 1] }).catch(() => {
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}, [safeIndex, phases, callTool, toolName]);
|
|
488
|
+
const prev = React.useCallback(() => {
|
|
489
|
+
if (safeIndex > 0) {
|
|
490
|
+
callTool(toolName, { phase: phases[safeIndex - 1] }).catch(() => {
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}, [safeIndex, phases, callTool, toolName]);
|
|
494
|
+
const goTo = React.useCallback(
|
|
495
|
+
(phase) => {
|
|
496
|
+
if (phases.includes(phase)) {
|
|
497
|
+
callTool(toolName, { phase }).catch(() => {
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
[phases, callTool, toolName]
|
|
502
|
+
);
|
|
503
|
+
const is = React.useCallback(
|
|
504
|
+
(phase) => current === phase,
|
|
505
|
+
[current]
|
|
506
|
+
);
|
|
507
|
+
return {
|
|
508
|
+
current,
|
|
509
|
+
index: safeIndex,
|
|
510
|
+
isFirst: safeIndex === 0,
|
|
511
|
+
isLast: safeIndex === phases.length - 1,
|
|
512
|
+
next,
|
|
513
|
+
prev,
|
|
514
|
+
goTo,
|
|
515
|
+
is
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function useBlob(blobKey, serverUrl) {
|
|
519
|
+
const [data, setData] = React.useState(null);
|
|
520
|
+
const cacheRef = React.useRef(/* @__PURE__ */ new Map());
|
|
521
|
+
React.useEffect(() => {
|
|
522
|
+
if (!blobKey) {
|
|
523
|
+
setData(null);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const cached = cacheRef.current.get(blobKey);
|
|
527
|
+
if (cached) {
|
|
528
|
+
setData(cached);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const baseUrl = serverUrl || (typeof window !== "undefined" ? window.location.origin : "http://localhost:4321");
|
|
532
|
+
let cancelled = false;
|
|
533
|
+
fetch(`${baseUrl}/blobs/${encodeURIComponent(blobKey)}`).then((res) => {
|
|
534
|
+
if (!res.ok) throw new Error(`Blob not found: ${blobKey}`);
|
|
535
|
+
return res.arrayBuffer();
|
|
536
|
+
}).then((buf) => {
|
|
537
|
+
if (!cancelled) {
|
|
538
|
+
cacheRef.current.set(blobKey, buf);
|
|
539
|
+
setData(buf);
|
|
540
|
+
}
|
|
541
|
+
}).catch(() => {
|
|
542
|
+
if (!cancelled) setData(null);
|
|
543
|
+
});
|
|
544
|
+
return () => {
|
|
545
|
+
cancelled = true;
|
|
546
|
+
};
|
|
547
|
+
}, [blobKey, serverUrl]);
|
|
548
|
+
return data;
|
|
549
|
+
}
|
|
274
550
|
|
|
275
551
|
// src/components.ts
|
|
276
552
|
function getReact2() {
|
|
@@ -404,6 +680,236 @@ function Grid({ children, columns = 2, gap = "0.75rem", style }) {
|
|
|
404
680
|
}
|
|
405
681
|
}, children);
|
|
406
682
|
}
|
|
683
|
+
function Slider({ value = 50, onChange, min = 0, max = 100, step = 1, disabled, label, style }) {
|
|
684
|
+
const pct = (value - min) / (max - min) * 100;
|
|
685
|
+
return h(
|
|
686
|
+
"div",
|
|
687
|
+
{ style: { display: "flex", flexDirection: "column", gap: "0.25rem", ...style } },
|
|
688
|
+
label ? h("div", {
|
|
689
|
+
style: { display: "flex", justifyContent: "space-between", fontSize: "0.8125rem", color: "#6b7280" }
|
|
690
|
+
}, h("span", null, label), h("span", null, String(value))) : null,
|
|
691
|
+
h("input", {
|
|
692
|
+
type: "range",
|
|
693
|
+
min,
|
|
694
|
+
max,
|
|
695
|
+
step,
|
|
696
|
+
value,
|
|
697
|
+
disabled,
|
|
698
|
+
onChange: onChange ? (e) => onChange(parseFloat(e.target.value)) : void 0,
|
|
699
|
+
style: {
|
|
700
|
+
width: "100%",
|
|
701
|
+
height: "6px",
|
|
702
|
+
appearance: "none",
|
|
703
|
+
WebkitAppearance: "none",
|
|
704
|
+
background: `linear-gradient(to right, #6366f1 ${pct}%, #d1d5db ${pct}%)`,
|
|
705
|
+
borderRadius: "3px",
|
|
706
|
+
outline: "none",
|
|
707
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
708
|
+
opacity: disabled ? 0.5 : 1
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function Textarea({ value, onChange, placeholder, rows = 3, disabled, style }) {
|
|
714
|
+
return h("textarea", {
|
|
715
|
+
value,
|
|
716
|
+
placeholder,
|
|
717
|
+
rows,
|
|
718
|
+
disabled,
|
|
719
|
+
onChange: onChange ? (e) => onChange(e.target.value) : void 0,
|
|
720
|
+
style: {
|
|
721
|
+
width: "100%",
|
|
722
|
+
padding: "0.5rem 0.75rem",
|
|
723
|
+
fontSize: "0.875rem",
|
|
724
|
+
border: "1px solid #d1d5db",
|
|
725
|
+
borderRadius: "0.5rem",
|
|
726
|
+
outline: "none",
|
|
727
|
+
backgroundColor: disabled ? "#f9fafb" : "#fff",
|
|
728
|
+
color: "#111827",
|
|
729
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
730
|
+
lineHeight: 1.5,
|
|
731
|
+
boxSizing: "border-box",
|
|
732
|
+
resize: "vertical",
|
|
733
|
+
...style
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function Modal({ children, open = false, onClose, title, style }) {
|
|
738
|
+
if (!open) return null;
|
|
739
|
+
return h(
|
|
740
|
+
"div",
|
|
741
|
+
{
|
|
742
|
+
onClick: onClose,
|
|
743
|
+
style: {
|
|
744
|
+
position: "fixed",
|
|
745
|
+
inset: 0,
|
|
746
|
+
zIndex: 1e4,
|
|
747
|
+
display: "flex",
|
|
748
|
+
alignItems: "center",
|
|
749
|
+
justifyContent: "center",
|
|
750
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
751
|
+
backdropFilter: "blur(4px)"
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
h(
|
|
755
|
+
"div",
|
|
756
|
+
{
|
|
757
|
+
onClick: (e) => e.stopPropagation(),
|
|
758
|
+
style: {
|
|
759
|
+
backgroundColor: "#fff",
|
|
760
|
+
borderRadius: "0.75rem",
|
|
761
|
+
padding: "1.5rem",
|
|
762
|
+
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
|
763
|
+
maxWidth: "480px",
|
|
764
|
+
width: "90%",
|
|
765
|
+
maxHeight: "80vh",
|
|
766
|
+
overflowY: "auto",
|
|
767
|
+
...style
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
title ? h(
|
|
771
|
+
"div",
|
|
772
|
+
{
|
|
773
|
+
style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1rem" }
|
|
774
|
+
},
|
|
775
|
+
h("h3", { style: { margin: 0, fontSize: "1.1rem", fontWeight: 600, color: "#111827" } }, title),
|
|
776
|
+
onClose ? h("button", {
|
|
777
|
+
onClick: onClose,
|
|
778
|
+
style: {
|
|
779
|
+
background: "none",
|
|
780
|
+
border: "none",
|
|
781
|
+
fontSize: "1.25rem",
|
|
782
|
+
cursor: "pointer",
|
|
783
|
+
color: "#6b7280",
|
|
784
|
+
padding: "0.25rem"
|
|
785
|
+
}
|
|
786
|
+
}, "\u2715") : null
|
|
787
|
+
) : null,
|
|
788
|
+
children
|
|
789
|
+
)
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
var DEFAULT_COLORS = [
|
|
793
|
+
"#ef4444",
|
|
794
|
+
"#f97316",
|
|
795
|
+
"#eab308",
|
|
796
|
+
"#22c55e",
|
|
797
|
+
"#06b6d4",
|
|
798
|
+
"#3b82f6",
|
|
799
|
+
"#6366f1",
|
|
800
|
+
"#8b5cf6",
|
|
801
|
+
"#ec4899",
|
|
802
|
+
"#111827",
|
|
803
|
+
"#6b7280",
|
|
804
|
+
"#ffffff"
|
|
805
|
+
];
|
|
806
|
+
function ColorPicker({ value = "#6366f1", onChange, presets, disabled, style }) {
|
|
807
|
+
const colors = presets || DEFAULT_COLORS;
|
|
808
|
+
return h(
|
|
809
|
+
"div",
|
|
810
|
+
{
|
|
811
|
+
style: { display: "flex", flexWrap: "wrap", gap: "0.375rem", alignItems: "center", ...style }
|
|
812
|
+
},
|
|
813
|
+
...colors.map(
|
|
814
|
+
(color) => h("button", {
|
|
815
|
+
key: color,
|
|
816
|
+
onClick: !disabled && onChange ? () => onChange(color) : void 0,
|
|
817
|
+
style: {
|
|
818
|
+
width: "28px",
|
|
819
|
+
height: "28px",
|
|
820
|
+
borderRadius: "50%",
|
|
821
|
+
border: color === value ? "2px solid #111827" : "2px solid transparent",
|
|
822
|
+
backgroundColor: color,
|
|
823
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
824
|
+
outline: color === value ? "2px solid #6366f1" : "none",
|
|
825
|
+
outlineOffset: "2px",
|
|
826
|
+
opacity: disabled ? 0.5 : 1,
|
|
827
|
+
padding: 0
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
),
|
|
831
|
+
h("input", {
|
|
832
|
+
type: "color",
|
|
833
|
+
value,
|
|
834
|
+
disabled,
|
|
835
|
+
onChange: onChange ? (e) => onChange(e.target.value) : void 0,
|
|
836
|
+
style: {
|
|
837
|
+
width: "28px",
|
|
838
|
+
height: "28px",
|
|
839
|
+
padding: 0,
|
|
840
|
+
border: "none",
|
|
841
|
+
borderRadius: "50%",
|
|
842
|
+
cursor: disabled ? "not-allowed" : "pointer"
|
|
843
|
+
}
|
|
844
|
+
})
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
function Dropdown({ value, onChange, options, placeholder, disabled, style }) {
|
|
848
|
+
return h(
|
|
849
|
+
"select",
|
|
850
|
+
{
|
|
851
|
+
value: value || "",
|
|
852
|
+
disabled,
|
|
853
|
+
onChange: onChange ? (e) => onChange(e.target.value) : void 0,
|
|
854
|
+
style: {
|
|
855
|
+
width: "100%",
|
|
856
|
+
padding: "0.5rem 0.75rem",
|
|
857
|
+
fontSize: "0.875rem",
|
|
858
|
+
border: "1px solid #d1d5db",
|
|
859
|
+
borderRadius: "0.5rem",
|
|
860
|
+
outline: "none",
|
|
861
|
+
backgroundColor: disabled ? "#f9fafb" : "#fff",
|
|
862
|
+
color: "#111827",
|
|
863
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
864
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
865
|
+
appearance: "none",
|
|
866
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%236b7280' stroke-width='1.5'/%3E%3C/svg%3E")`,
|
|
867
|
+
backgroundRepeat: "no-repeat",
|
|
868
|
+
backgroundPosition: "right 0.75rem center",
|
|
869
|
+
paddingRight: "2rem",
|
|
870
|
+
...style
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
placeholder ? h("option", { value: "", disabled: true }, placeholder) : null,
|
|
874
|
+
...options.map((opt) => h("option", { key: opt.value, value: opt.value }, opt.label))
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
function Tabs({ tabs, activeTab, onTabChange, children, style }) {
|
|
878
|
+
const active = activeTab || tabs[0]?.id;
|
|
879
|
+
return h(
|
|
880
|
+
"div",
|
|
881
|
+
{ style: { ...style } },
|
|
882
|
+
h(
|
|
883
|
+
"div",
|
|
884
|
+
{
|
|
885
|
+
style: {
|
|
886
|
+
display: "flex",
|
|
887
|
+
borderBottom: "1px solid #e5e7eb",
|
|
888
|
+
gap: 0
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
...tabs.map(
|
|
892
|
+
(tab) => h("button", {
|
|
893
|
+
key: tab.id,
|
|
894
|
+
onClick: onTabChange ? () => onTabChange(tab.id) : void 0,
|
|
895
|
+
style: {
|
|
896
|
+
padding: "0.5rem 1rem",
|
|
897
|
+
fontSize: "0.8125rem",
|
|
898
|
+
fontWeight: 500,
|
|
899
|
+
background: "none",
|
|
900
|
+
border: "none",
|
|
901
|
+
cursor: "pointer",
|
|
902
|
+
color: tab.id === active ? "#6366f1" : "#6b7280",
|
|
903
|
+
borderBottom: tab.id === active ? "2px solid #6366f1" : "2px solid transparent",
|
|
904
|
+
marginBottom: "-1px",
|
|
905
|
+
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
906
|
+
}
|
|
907
|
+
}, tab.label)
|
|
908
|
+
)
|
|
909
|
+
),
|
|
910
|
+
h("div", { style: { paddingTop: "0.75rem" } }, children)
|
|
911
|
+
);
|
|
912
|
+
}
|
|
407
913
|
|
|
408
914
|
// src/agent-protocol.ts
|
|
409
915
|
function uid() {
|
|
@@ -559,45 +1065,6 @@ function createAgentProtocolTools(namespace, z) {
|
|
|
559
1065
|
];
|
|
560
1066
|
}
|
|
561
1067
|
|
|
562
|
-
// src/agent-hints.ts
|
|
563
|
-
function createAgentProtocolHints(namespace) {
|
|
564
|
-
return [
|
|
565
|
-
{
|
|
566
|
-
trigger: "A proposal is pending and you have not voted on it yet",
|
|
567
|
-
condition: `Object.values(state._agentProposals || {}).some(p => p.status === 'pending' && !p.votes.some(v => v.actorId === actorId))`,
|
|
568
|
-
suggestedTools: [`${namespace}.agent.vote`],
|
|
569
|
-
priority: "high",
|
|
570
|
-
cooldownMs: 2e3
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
trigger: "You received a delegation addressed to you",
|
|
574
|
-
condition: `(state._agentMessages || []).some(m => m.type === 'delegate' && m.to === actorId)`,
|
|
575
|
-
suggestedTools: [`${namespace}.agent.respond`],
|
|
576
|
-
priority: "high",
|
|
577
|
-
cooldownMs: 3e3
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
trigger: "You received a request addressed to you",
|
|
581
|
-
condition: `(state._agentMessages || []).some(m => m.type === 'request' && m.to === actorId)`,
|
|
582
|
-
suggestedTools: [`${namespace}.agent.respond`],
|
|
583
|
-
priority: "medium",
|
|
584
|
-
cooldownMs: 2e3
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
trigger: "You have useful information to share with other agents",
|
|
588
|
-
suggestedTools: [`${namespace}.agent.inform`],
|
|
589
|
-
priority: "low",
|
|
590
|
-
cooldownMs: 5e3
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
trigger: "You want to propose a collaborative action for the group",
|
|
594
|
-
suggestedTools: [`${namespace}.agent.propose`],
|
|
595
|
-
priority: "low",
|
|
596
|
-
cooldownMs: 1e4
|
|
597
|
-
}
|
|
598
|
-
];
|
|
599
|
-
}
|
|
600
|
-
|
|
601
1068
|
// src/migrations.ts
|
|
602
1069
|
function getStateVersion(state) {
|
|
603
1070
|
return typeof state._version === "string" ? state._version : "0.0.0";
|
|
@@ -691,30 +1158,4345 @@ var InMemoryAdapter = class {
|
|
|
691
1158
|
return this.profiles.get(userId) || null;
|
|
692
1159
|
}
|
|
693
1160
|
};
|
|
1161
|
+
|
|
1162
|
+
// src/scene/renderer-svg.ts
|
|
1163
|
+
function getReact3() {
|
|
1164
|
+
const R = globalThis.React;
|
|
1165
|
+
if (!R) throw new Error("React is not available.");
|
|
1166
|
+
return R;
|
|
1167
|
+
}
|
|
1168
|
+
function h2(type, props, ...children) {
|
|
1169
|
+
return getReact3().createElement(type, props, ...children);
|
|
1170
|
+
}
|
|
1171
|
+
function buildTransformString(t) {
|
|
1172
|
+
if (!t) return void 0;
|
|
1173
|
+
const parts = [];
|
|
1174
|
+
if (t.x != null || t.y != null) {
|
|
1175
|
+
parts.push(`translate(${t.x ?? 0}, ${t.y ?? 0})`);
|
|
1176
|
+
}
|
|
1177
|
+
if (t.rotation != null && t.rotation !== 0) {
|
|
1178
|
+
parts.push(`rotate(${t.rotation})`);
|
|
1179
|
+
}
|
|
1180
|
+
if (t.scaleX != null && t.scaleX !== 1 || t.scaleY != null && t.scaleY !== 1) {
|
|
1181
|
+
parts.push(`scale(${t.scaleX ?? 1}, ${t.scaleY ?? 1})`);
|
|
1182
|
+
}
|
|
1183
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
1184
|
+
}
|
|
1185
|
+
function buildStyleAttrs(s) {
|
|
1186
|
+
if (!s) return {};
|
|
1187
|
+
const attrs = {};
|
|
1188
|
+
if (s.fill != null) attrs.fill = s.fill;
|
|
1189
|
+
if (s.stroke != null) attrs.stroke = s.stroke;
|
|
1190
|
+
if (s.strokeWidth != null) attrs.strokeWidth = s.strokeWidth;
|
|
1191
|
+
if (s.strokeDasharray != null) attrs.strokeDasharray = s.strokeDasharray;
|
|
1192
|
+
if (s.strokeLinecap != null) attrs.strokeLinecap = s.strokeLinecap;
|
|
1193
|
+
if (s.strokeLinejoin != null) attrs.strokeLinejoin = s.strokeLinejoin;
|
|
1194
|
+
if (s.opacity != null) attrs.opacity = s.opacity;
|
|
1195
|
+
if (s.fillOpacity != null) attrs.fillOpacity = s.fillOpacity;
|
|
1196
|
+
if (s.strokeOpacity != null) attrs.strokeOpacity = s.strokeOpacity;
|
|
1197
|
+
if (s.filter != null) attrs.filter = s.filter;
|
|
1198
|
+
if (s.cursor != null) attrs.style = { ...attrs.style, cursor: s.cursor };
|
|
1199
|
+
if (s.pointerEvents != null) attrs.pointerEvents = s.pointerEvents;
|
|
1200
|
+
return attrs;
|
|
1201
|
+
}
|
|
1202
|
+
function buildTextAttrs(s) {
|
|
1203
|
+
if (!s) return {};
|
|
1204
|
+
const attrs = buildStyleAttrs(s);
|
|
1205
|
+
if (s.fontSize != null) attrs.fontSize = s.fontSize;
|
|
1206
|
+
if (s.fontFamily != null) attrs.fontFamily = s.fontFamily;
|
|
1207
|
+
if (s.fontWeight != null) attrs.fontWeight = s.fontWeight;
|
|
1208
|
+
if (s.textAnchor != null) attrs.textAnchor = s.textAnchor;
|
|
1209
|
+
if (s.dominantBaseline != null) attrs.dominantBaseline = s.dominantBaseline;
|
|
1210
|
+
if (s.letterSpacing != null) attrs.letterSpacing = s.letterSpacing;
|
|
1211
|
+
return attrs;
|
|
1212
|
+
}
|
|
1213
|
+
function renderGradient(g) {
|
|
1214
|
+
if (g.type === "linear") {
|
|
1215
|
+
const lg = g;
|
|
1216
|
+
return h2(
|
|
1217
|
+
"linearGradient",
|
|
1218
|
+
{
|
|
1219
|
+
key: lg.id,
|
|
1220
|
+
id: lg.id,
|
|
1221
|
+
x1: lg.x1,
|
|
1222
|
+
y1: lg.y1,
|
|
1223
|
+
x2: lg.x2,
|
|
1224
|
+
y2: lg.y2,
|
|
1225
|
+
gradientUnits: "objectBoundingBox"
|
|
1226
|
+
},
|
|
1227
|
+
...lg.stops.map(
|
|
1228
|
+
(s, i) => h2("stop", { key: i, offset: s.offset, stopColor: s.color })
|
|
1229
|
+
)
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
const rg = g;
|
|
1233
|
+
return h2(
|
|
1234
|
+
"radialGradient",
|
|
1235
|
+
{
|
|
1236
|
+
key: rg.id,
|
|
1237
|
+
id: rg.id,
|
|
1238
|
+
cx: rg.cx,
|
|
1239
|
+
cy: rg.cy,
|
|
1240
|
+
r: rg.r,
|
|
1241
|
+
fx: rg.fx,
|
|
1242
|
+
fy: rg.fy,
|
|
1243
|
+
gradientUnits: "objectBoundingBox"
|
|
1244
|
+
},
|
|
1245
|
+
...rg.stops.map(
|
|
1246
|
+
(s, i) => h2("stop", { key: i, offset: s.offset, stopColor: s.color })
|
|
1247
|
+
)
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
function renderFilter(f) {
|
|
1251
|
+
const children = [];
|
|
1252
|
+
switch (f.type) {
|
|
1253
|
+
case "blur":
|
|
1254
|
+
children.push(h2("feGaussianBlur", { key: "blur", stdDeviation: f.params.radius ?? 4 }));
|
|
1255
|
+
break;
|
|
1256
|
+
case "shadow":
|
|
1257
|
+
children.push(
|
|
1258
|
+
h2("feDropShadow", {
|
|
1259
|
+
key: "shadow",
|
|
1260
|
+
dx: f.params.dx ?? 2,
|
|
1261
|
+
dy: f.params.dy ?? 2,
|
|
1262
|
+
stdDeviation: f.params.blur ?? 3,
|
|
1263
|
+
floodColor: f.params.color ?? "#000",
|
|
1264
|
+
floodOpacity: f.params.opacity ?? 0.5
|
|
1265
|
+
})
|
|
1266
|
+
);
|
|
1267
|
+
break;
|
|
1268
|
+
case "glow":
|
|
1269
|
+
children.push(
|
|
1270
|
+
h2("feGaussianBlur", { key: "blur", stdDeviation: f.params.radius ?? 4, result: "blur" }),
|
|
1271
|
+
h2(
|
|
1272
|
+
"feMerge",
|
|
1273
|
+
{ key: "merge" },
|
|
1274
|
+
h2("feMergeNode", { key: "n1", in: "blur" }),
|
|
1275
|
+
h2("feMergeNode", { key: "n2", in: "SourceGraphic" })
|
|
1276
|
+
)
|
|
1277
|
+
);
|
|
1278
|
+
break;
|
|
1279
|
+
default:
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
return h2("filter", { key: f.id, id: f.id }, ...children);
|
|
1283
|
+
}
|
|
1284
|
+
function interactionHandlers(node, rctx) {
|
|
1285
|
+
if (!node.interactive) return {};
|
|
1286
|
+
const handlers = {};
|
|
1287
|
+
if (rctx.onNodeClick) {
|
|
1288
|
+
handlers.onClick = (e) => {
|
|
1289
|
+
e.stopPropagation();
|
|
1290
|
+
const rect = e.currentTarget.ownerSVGElement?.getBoundingClientRect();
|
|
1291
|
+
rctx.onNodeClick(node.id, {
|
|
1292
|
+
x: rect ? e.clientX - rect.left : e.clientX,
|
|
1293
|
+
y: rect ? e.clientY - rect.top : e.clientY
|
|
1294
|
+
});
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
if (rctx.onNodeHover) {
|
|
1298
|
+
handlers.onMouseEnter = () => rctx.onNodeHover(node.id);
|
|
1299
|
+
handlers.onMouseLeave = () => rctx.onNodeHover(null);
|
|
1300
|
+
}
|
|
1301
|
+
if (rctx.onNodeDragStart) {
|
|
1302
|
+
handlers.onMouseDown = (e) => {
|
|
1303
|
+
e.stopPropagation();
|
|
1304
|
+
const rect = e.currentTarget.ownerSVGElement?.getBoundingClientRect();
|
|
1305
|
+
const pos = {
|
|
1306
|
+
x: rect ? e.clientX - rect.left : e.clientX,
|
|
1307
|
+
y: rect ? e.clientY - rect.top : e.clientY
|
|
1308
|
+
};
|
|
1309
|
+
rctx.onNodeDragStart(node.id, pos);
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
return handlers;
|
|
1313
|
+
}
|
|
1314
|
+
function renderNode(node, rctx) {
|
|
1315
|
+
if (node.style?.visible === false) return null;
|
|
1316
|
+
const transform = buildTransformString(node.transform);
|
|
1317
|
+
const handlers = interactionHandlers(node, rctx);
|
|
1318
|
+
const isSelected = rctx.selectedNodeIds?.includes(node.id);
|
|
1319
|
+
let element = null;
|
|
1320
|
+
switch (node.type) {
|
|
1321
|
+
case "rect": {
|
|
1322
|
+
const n = node;
|
|
1323
|
+
element = h2("rect", {
|
|
1324
|
+
x: -n.width / 2,
|
|
1325
|
+
y: -n.height / 2,
|
|
1326
|
+
width: n.width,
|
|
1327
|
+
height: n.height,
|
|
1328
|
+
rx: n.rx,
|
|
1329
|
+
ry: n.ry,
|
|
1330
|
+
...buildStyleAttrs(n.style),
|
|
1331
|
+
...handlers
|
|
1332
|
+
});
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
case "circle": {
|
|
1336
|
+
const n = node;
|
|
1337
|
+
element = h2("circle", {
|
|
1338
|
+
r: n.radius,
|
|
1339
|
+
...buildStyleAttrs(n.style),
|
|
1340
|
+
...handlers
|
|
1341
|
+
});
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
case "ellipse": {
|
|
1345
|
+
const n = node;
|
|
1346
|
+
element = h2("ellipse", {
|
|
1347
|
+
rx: n.rx,
|
|
1348
|
+
ry: n.ry,
|
|
1349
|
+
...buildStyleAttrs(n.style),
|
|
1350
|
+
...handlers
|
|
1351
|
+
});
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
case "line": {
|
|
1355
|
+
const n = node;
|
|
1356
|
+
element = h2("line", {
|
|
1357
|
+
x1: 0,
|
|
1358
|
+
y1: 0,
|
|
1359
|
+
x2: n.x2,
|
|
1360
|
+
y2: n.y2,
|
|
1361
|
+
...buildStyleAttrs(n.style),
|
|
1362
|
+
...handlers
|
|
1363
|
+
});
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
case "polyline": {
|
|
1367
|
+
const n = node;
|
|
1368
|
+
element = h2("polyline", {
|
|
1369
|
+
points: n.points.map((p) => `${p.x},${p.y}`).join(" "),
|
|
1370
|
+
fill: "none",
|
|
1371
|
+
...buildStyleAttrs(n.style),
|
|
1372
|
+
...handlers
|
|
1373
|
+
});
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case "polygon": {
|
|
1377
|
+
const n = node;
|
|
1378
|
+
element = h2("polygon", {
|
|
1379
|
+
points: n.points.map((p) => `${p.x},${p.y}`).join(" "),
|
|
1380
|
+
...buildStyleAttrs(n.style),
|
|
1381
|
+
...handlers
|
|
1382
|
+
});
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
case "path": {
|
|
1386
|
+
const n = node;
|
|
1387
|
+
element = h2("path", {
|
|
1388
|
+
d: n.d,
|
|
1389
|
+
...buildStyleAttrs(n.style),
|
|
1390
|
+
...handlers
|
|
1391
|
+
});
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
case "text": {
|
|
1395
|
+
const n = node;
|
|
1396
|
+
element = h2("text", {
|
|
1397
|
+
...buildTextAttrs(n.style),
|
|
1398
|
+
...handlers
|
|
1399
|
+
}, n.text);
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
case "image": {
|
|
1403
|
+
const n = node;
|
|
1404
|
+
element = h2("image", {
|
|
1405
|
+
href: n.href,
|
|
1406
|
+
x: -n.width / 2,
|
|
1407
|
+
y: -n.height / 2,
|
|
1408
|
+
width: n.width,
|
|
1409
|
+
height: n.height,
|
|
1410
|
+
preserveAspectRatio: n.preserveAspectRatio ?? "xMidYMid meet",
|
|
1411
|
+
...handlers
|
|
1412
|
+
});
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
case "group": {
|
|
1416
|
+
const n = node;
|
|
1417
|
+
element = h2(
|
|
1418
|
+
"g",
|
|
1419
|
+
{},
|
|
1420
|
+
...n.children.map((child) => renderNode(child, rctx))
|
|
1421
|
+
);
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
case "sprite": {
|
|
1425
|
+
const n = node;
|
|
1426
|
+
const cols = n.columns ?? Math.floor(1e3 / n.frameWidth);
|
|
1427
|
+
const frame = n.frame ?? 0;
|
|
1428
|
+
const col = frame % cols;
|
|
1429
|
+
const row = Math.floor(frame / cols);
|
|
1430
|
+
const clipId = `sprite-clip-${n.id}`;
|
|
1431
|
+
element = h2(
|
|
1432
|
+
"g",
|
|
1433
|
+
{},
|
|
1434
|
+
h2(
|
|
1435
|
+
"defs",
|
|
1436
|
+
{},
|
|
1437
|
+
h2(
|
|
1438
|
+
"clipPath",
|
|
1439
|
+
{ id: clipId },
|
|
1440
|
+
h2("rect", { x: 0, y: 0, width: n.frameWidth, height: n.frameHeight })
|
|
1441
|
+
)
|
|
1442
|
+
),
|
|
1443
|
+
h2(
|
|
1444
|
+
"g",
|
|
1445
|
+
{ clipPath: `url(#${clipId})` },
|
|
1446
|
+
h2("image", {
|
|
1447
|
+
href: n.href,
|
|
1448
|
+
x: -col * n.frameWidth,
|
|
1449
|
+
y: -row * n.frameHeight,
|
|
1450
|
+
width: cols * n.frameWidth,
|
|
1451
|
+
height: Math.ceil(1e3 / cols) * n.frameHeight,
|
|
1452
|
+
// Approximate
|
|
1453
|
+
...handlers
|
|
1454
|
+
})
|
|
1455
|
+
)
|
|
1456
|
+
);
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
case "tilemap": {
|
|
1460
|
+
const n = node;
|
|
1461
|
+
const tiles = [];
|
|
1462
|
+
for (let row = 0; row < n.height; row++) {
|
|
1463
|
+
for (let col = 0; col < n.width; col++) {
|
|
1464
|
+
const tileIdx = n.data[row]?.[col];
|
|
1465
|
+
if (tileIdx == null || tileIdx < 0) continue;
|
|
1466
|
+
const srcCol = tileIdx % n.columns;
|
|
1467
|
+
const srcRow = Math.floor(tileIdx / n.columns);
|
|
1468
|
+
const clipId = `tile-${n.id}-${row}-${col}`;
|
|
1469
|
+
tiles.push(
|
|
1470
|
+
h2(
|
|
1471
|
+
"g",
|
|
1472
|
+
{
|
|
1473
|
+
key: `${row}-${col}`,
|
|
1474
|
+
transform: `translate(${col * n.tileWidth}, ${row * n.tileHeight})`
|
|
1475
|
+
},
|
|
1476
|
+
h2(
|
|
1477
|
+
"defs",
|
|
1478
|
+
{},
|
|
1479
|
+
h2(
|
|
1480
|
+
"clipPath",
|
|
1481
|
+
{ id: clipId },
|
|
1482
|
+
h2("rect", { x: 0, y: 0, width: n.tileWidth, height: n.tileHeight })
|
|
1483
|
+
)
|
|
1484
|
+
),
|
|
1485
|
+
h2(
|
|
1486
|
+
"g",
|
|
1487
|
+
{ clipPath: `url(#${clipId})` },
|
|
1488
|
+
h2("image", {
|
|
1489
|
+
href: n.href,
|
|
1490
|
+
x: -srcCol * n.tileWidth,
|
|
1491
|
+
y: -srcRow * n.tileHeight,
|
|
1492
|
+
width: n.columns * n.tileWidth,
|
|
1493
|
+
height: Math.ceil(256 / n.columns) * n.tileHeight
|
|
1494
|
+
})
|
|
1495
|
+
)
|
|
1496
|
+
)
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
element = h2("g", {}, ...tiles);
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
case "particles": {
|
|
1504
|
+
const n = node;
|
|
1505
|
+
const particles = n._particles ?? [];
|
|
1506
|
+
element = h2(
|
|
1507
|
+
"g",
|
|
1508
|
+
{},
|
|
1509
|
+
...particles.map((p, i) => {
|
|
1510
|
+
const alpha = n.emitters[0]?.fadeOut !== false ? Math.max(0, 1 - p.age / p.lifetime) : 1;
|
|
1511
|
+
const shape = n.emitters[0]?.shape ?? "circle";
|
|
1512
|
+
if (shape === "square") {
|
|
1513
|
+
return h2("rect", {
|
|
1514
|
+
key: i,
|
|
1515
|
+
x: p.x - p.size / 2,
|
|
1516
|
+
y: p.y - p.size / 2,
|
|
1517
|
+
width: p.size,
|
|
1518
|
+
height: p.size,
|
|
1519
|
+
fill: p.color,
|
|
1520
|
+
opacity: alpha
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return h2("circle", {
|
|
1524
|
+
key: i,
|
|
1525
|
+
cx: p.x,
|
|
1526
|
+
cy: p.y,
|
|
1527
|
+
r: p.size / 2,
|
|
1528
|
+
fill: p.color,
|
|
1529
|
+
opacity: alpha
|
|
1530
|
+
});
|
|
1531
|
+
})
|
|
1532
|
+
);
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
default:
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
if (!element) return null;
|
|
1539
|
+
const wrapperProps = { key: node.id };
|
|
1540
|
+
if (transform) wrapperProps.transform = transform;
|
|
1541
|
+
const children = [element];
|
|
1542
|
+
if (isSelected && node.type !== "group") {
|
|
1543
|
+
children.push(
|
|
1544
|
+
h2("rect", {
|
|
1545
|
+
key: "selection",
|
|
1546
|
+
x: -getBoundsWidth(node) / 2 - 4,
|
|
1547
|
+
y: -getBoundsHeight(node) / 2 - 4,
|
|
1548
|
+
width: getBoundsWidth(node) + 8,
|
|
1549
|
+
height: getBoundsHeight(node) + 8,
|
|
1550
|
+
fill: "none",
|
|
1551
|
+
stroke: "#6366f1",
|
|
1552
|
+
strokeWidth: 2,
|
|
1553
|
+
strokeDasharray: "4,2",
|
|
1554
|
+
pointerEvents: "none"
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (rctx.debug) {
|
|
1559
|
+
children.push(
|
|
1560
|
+
h2("text", {
|
|
1561
|
+
key: "debug-label",
|
|
1562
|
+
y: -getBoundsHeight(node) / 2 - 8,
|
|
1563
|
+
fontSize: 10,
|
|
1564
|
+
fill: "#94a3b8",
|
|
1565
|
+
textAnchor: "middle",
|
|
1566
|
+
pointerEvents: "none"
|
|
1567
|
+
}, node.id)
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
return h2("g", wrapperProps, ...children);
|
|
1571
|
+
}
|
|
1572
|
+
function getBoundsWidth(node) {
|
|
1573
|
+
switch (node.type) {
|
|
1574
|
+
case "rect":
|
|
1575
|
+
return node.width;
|
|
1576
|
+
case "circle":
|
|
1577
|
+
return node.radius * 2;
|
|
1578
|
+
case "ellipse":
|
|
1579
|
+
return node.rx * 2;
|
|
1580
|
+
case "image":
|
|
1581
|
+
return node.width;
|
|
1582
|
+
case "text":
|
|
1583
|
+
return 100;
|
|
1584
|
+
// Approximate
|
|
1585
|
+
default:
|
|
1586
|
+
return 50;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function getBoundsHeight(node) {
|
|
1590
|
+
switch (node.type) {
|
|
1591
|
+
case "rect":
|
|
1592
|
+
return node.height;
|
|
1593
|
+
case "circle":
|
|
1594
|
+
return node.radius * 2;
|
|
1595
|
+
case "ellipse":
|
|
1596
|
+
return node.ry * 2;
|
|
1597
|
+
case "image":
|
|
1598
|
+
return node.height;
|
|
1599
|
+
case "text":
|
|
1600
|
+
return 24;
|
|
1601
|
+
// Approximate
|
|
1602
|
+
default:
|
|
1603
|
+
return 50;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function SvgSceneRenderer(props) {
|
|
1607
|
+
const {
|
|
1608
|
+
scene,
|
|
1609
|
+
width = scene.width ?? 800,
|
|
1610
|
+
height = scene.height ?? 600,
|
|
1611
|
+
className,
|
|
1612
|
+
style: containerStyle,
|
|
1613
|
+
onNodeClick,
|
|
1614
|
+
onNodeHover,
|
|
1615
|
+
onNodeDragStart,
|
|
1616
|
+
onNodeDrag,
|
|
1617
|
+
onNodeDragEnd,
|
|
1618
|
+
onViewportClick,
|
|
1619
|
+
onViewportPan,
|
|
1620
|
+
onViewportZoom,
|
|
1621
|
+
selectedNodeIds,
|
|
1622
|
+
debug
|
|
1623
|
+
} = props;
|
|
1624
|
+
const React4 = getReact3();
|
|
1625
|
+
const camera = scene.camera ?? { x: width / 2, y: height / 2, zoom: 1 };
|
|
1626
|
+
const zoom = camera.zoom || 1;
|
|
1627
|
+
const vbW = width / zoom;
|
|
1628
|
+
const vbH = height / zoom;
|
|
1629
|
+
const vbX = camera.x - vbW / 2;
|
|
1630
|
+
const vbY = camera.y - vbH / 2;
|
|
1631
|
+
const viewBox = `${vbX} ${vbY} ${vbW} ${vbH}`;
|
|
1632
|
+
const dragRef = React4.useRef(null);
|
|
1633
|
+
const rctx = {
|
|
1634
|
+
onNodeClick,
|
|
1635
|
+
onNodeHover,
|
|
1636
|
+
onNodeDragStart,
|
|
1637
|
+
onNodeDrag,
|
|
1638
|
+
onNodeDragEnd,
|
|
1639
|
+
selectedNodeIds,
|
|
1640
|
+
debug
|
|
1641
|
+
};
|
|
1642
|
+
const svgHandlers = {};
|
|
1643
|
+
if (onViewportClick) {
|
|
1644
|
+
svgHandlers.onClick = (e) => {
|
|
1645
|
+
const svg = e.currentTarget;
|
|
1646
|
+
const rect = svg.getBoundingClientRect();
|
|
1647
|
+
const x = vbX + (e.clientX - rect.left) / rect.width * vbW;
|
|
1648
|
+
const y = vbY + (e.clientY - rect.top) / rect.height * vbH;
|
|
1649
|
+
onViewportClick({ x, y });
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
if (onViewportZoom) {
|
|
1653
|
+
svgHandlers.onWheel = (e) => {
|
|
1654
|
+
e.preventDefault();
|
|
1655
|
+
const svg = e.currentTarget;
|
|
1656
|
+
const rect = svg.getBoundingClientRect();
|
|
1657
|
+
const cx = vbX + (e.clientX - rect.left) / rect.width * vbW;
|
|
1658
|
+
const cy = vbY + (e.clientY - rect.top) / rect.height * vbH;
|
|
1659
|
+
const factor = e.deltaY > 0 ? 0.9 : 1.1;
|
|
1660
|
+
onViewportZoom(zoom * factor, { x: cx, y: cy });
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (onViewportPan) {
|
|
1664
|
+
svgHandlers.onMouseDown = (e) => {
|
|
1665
|
+
if (e.target === e.currentTarget || e.target.tagName === "rect") {
|
|
1666
|
+
dragRef.current = {
|
|
1667
|
+
startX: e.clientX,
|
|
1668
|
+
startY: e.clientY,
|
|
1669
|
+
camX: camera.x,
|
|
1670
|
+
camY: camera.y
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
svgHandlers.onMouseMove = (e) => {
|
|
1675
|
+
if (!dragRef.current) return;
|
|
1676
|
+
const dx = (e.clientX - dragRef.current.startX) / zoom;
|
|
1677
|
+
const dy = (e.clientY - dragRef.current.startY) / zoom;
|
|
1678
|
+
onViewportPan({ x: -dx, y: -dy });
|
|
1679
|
+
};
|
|
1680
|
+
svgHandlers.onMouseUp = () => {
|
|
1681
|
+
dragRef.current = null;
|
|
1682
|
+
};
|
|
1683
|
+
svgHandlers.onMouseLeave = () => {
|
|
1684
|
+
dragRef.current = null;
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
const defs = [];
|
|
1688
|
+
if (scene.gradients?.length) {
|
|
1689
|
+
for (const g of scene.gradients) {
|
|
1690
|
+
defs.push(renderGradient(g));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (scene.filters?.length) {
|
|
1694
|
+
for (const f of scene.filters) {
|
|
1695
|
+
defs.push(renderFilter(f));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return h2(
|
|
1699
|
+
"svg",
|
|
1700
|
+
{
|
|
1701
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1702
|
+
viewBox,
|
|
1703
|
+
width,
|
|
1704
|
+
height,
|
|
1705
|
+
className,
|
|
1706
|
+
style: {
|
|
1707
|
+
backgroundColor: scene.background ?? "#1a1a2e",
|
|
1708
|
+
display: "block",
|
|
1709
|
+
...containerStyle
|
|
1710
|
+
},
|
|
1711
|
+
...svgHandlers
|
|
1712
|
+
},
|
|
1713
|
+
defs.length > 0 ? h2("defs", { key: "defs" }, ...defs) : null,
|
|
1714
|
+
renderNode(scene.root, rctx)
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/scene/tweens.ts
|
|
1719
|
+
var easingFunctions = {
|
|
1720
|
+
"linear": (t) => t,
|
|
1721
|
+
"ease-in": (t) => t * t * t,
|
|
1722
|
+
"ease-out": (t) => 1 - Math.pow(1 - t, 3),
|
|
1723
|
+
"ease-in-out": (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
1724
|
+
"ease-in-quad": (t) => t * t,
|
|
1725
|
+
"ease-out-quad": (t) => 1 - (1 - t) * (1 - t),
|
|
1726
|
+
"ease-in-out-quad": (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
|
|
1727
|
+
"ease-in-cubic": (t) => t * t * t,
|
|
1728
|
+
"ease-out-cubic": (t) => 1 - Math.pow(1 - t, 3),
|
|
1729
|
+
"ease-in-out-cubic": (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
1730
|
+
"ease-in-elastic": (t) => {
|
|
1731
|
+
if (t === 0 || t === 1) return t;
|
|
1732
|
+
return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI / 3));
|
|
1733
|
+
},
|
|
1734
|
+
"ease-out-elastic": (t) => {
|
|
1735
|
+
if (t === 0 || t === 1) return t;
|
|
1736
|
+
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
|
|
1737
|
+
},
|
|
1738
|
+
"ease-in-bounce": (t) => 1 - bounceOut(1 - t),
|
|
1739
|
+
"ease-out-bounce": bounceOut
|
|
1740
|
+
};
|
|
1741
|
+
function bounceOut(t) {
|
|
1742
|
+
const n1 = 7.5625;
|
|
1743
|
+
const d1 = 2.75;
|
|
1744
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
1745
|
+
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
1746
|
+
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
1747
|
+
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
1748
|
+
}
|
|
1749
|
+
function getPath(obj, path) {
|
|
1750
|
+
const parts = path.split(".");
|
|
1751
|
+
let current = obj;
|
|
1752
|
+
for (const part of parts) {
|
|
1753
|
+
if (current == null) return void 0;
|
|
1754
|
+
current = current[part];
|
|
1755
|
+
}
|
|
1756
|
+
return current;
|
|
1757
|
+
}
|
|
1758
|
+
function setPath(obj, path, value) {
|
|
1759
|
+
const parts = path.split(".");
|
|
1760
|
+
if (parts.length === 1) {
|
|
1761
|
+
return { ...obj, [parts[0]]: value };
|
|
1762
|
+
}
|
|
1763
|
+
const [head, ...rest] = parts;
|
|
1764
|
+
return {
|
|
1765
|
+
...obj,
|
|
1766
|
+
[head]: setPath(obj?.[head] ?? {}, rest.join("."), value)
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function interpolateTween(tween, now) {
|
|
1770
|
+
if (tween.startedAt == null) return null;
|
|
1771
|
+
const delay = tween.delay ?? 0;
|
|
1772
|
+
const elapsed = now - tween.startedAt - delay;
|
|
1773
|
+
if (elapsed < 0) return tween.from;
|
|
1774
|
+
const repeat = tween.repeat ?? 0;
|
|
1775
|
+
const duration = tween.duration;
|
|
1776
|
+
if (duration <= 0) return tween.to;
|
|
1777
|
+
let iteration = Math.floor(elapsed / duration);
|
|
1778
|
+
let progress = elapsed % duration / duration;
|
|
1779
|
+
if (repeat >= 0 && iteration > repeat) {
|
|
1780
|
+
if (tween.yoyo && repeat % 2 === 0) return tween.from;
|
|
1781
|
+
return tween.to;
|
|
1782
|
+
}
|
|
1783
|
+
if (tween.yoyo && iteration % 2 === 1) {
|
|
1784
|
+
progress = 1 - progress;
|
|
1785
|
+
}
|
|
1786
|
+
const easingName = tween.easing ?? "ease-in-out";
|
|
1787
|
+
const easeFn = easingFunctions[easingName] ?? easingFunctions["linear"];
|
|
1788
|
+
const t = easeFn(Math.max(0, Math.min(1, progress)));
|
|
1789
|
+
return tween.from + (tween.to - tween.from) * t;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// src/scene/renderer-pixi.ts
|
|
1793
|
+
function getReact4() {
|
|
1794
|
+
const R = globalThis.React;
|
|
1795
|
+
if (!R) throw new Error("React is not available.");
|
|
1796
|
+
return R;
|
|
1797
|
+
}
|
|
1798
|
+
function h3(type, props, ...children) {
|
|
1799
|
+
return getReact4().createElement(type, props, ...children);
|
|
1800
|
+
}
|
|
1801
|
+
var _PIXI = null;
|
|
1802
|
+
var _pixiLoadAttempted = false;
|
|
1803
|
+
var _pixiLoadPromise = null;
|
|
1804
|
+
function getPixi() {
|
|
1805
|
+
if (_PIXI) return _PIXI;
|
|
1806
|
+
if (globalThis.__PIXI) {
|
|
1807
|
+
_PIXI = globalThis.__PIXI;
|
|
1808
|
+
return _PIXI;
|
|
1809
|
+
}
|
|
1810
|
+
if (globalThis.PIXI) {
|
|
1811
|
+
_PIXI = globalThis.PIXI;
|
|
1812
|
+
return _PIXI;
|
|
1813
|
+
}
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
async function loadPixi() {
|
|
1817
|
+
if (_PIXI) return _PIXI;
|
|
1818
|
+
if (_pixiLoadPromise) return _pixiLoadPromise;
|
|
1819
|
+
_pixiLoadPromise = (async () => {
|
|
1820
|
+
const g = getPixi();
|
|
1821
|
+
if (g) return g;
|
|
1822
|
+
if (_pixiLoadAttempted) return null;
|
|
1823
|
+
_pixiLoadAttempted = true;
|
|
1824
|
+
try {
|
|
1825
|
+
const mod = await import(
|
|
1826
|
+
/* webpackIgnore: true */
|
|
1827
|
+
"pixi.js"
|
|
1828
|
+
);
|
|
1829
|
+
_PIXI = mod.default || mod;
|
|
1830
|
+
return _PIXI;
|
|
1831
|
+
} catch {
|
|
1832
|
+
try {
|
|
1833
|
+
const mod = await import(
|
|
1834
|
+
/* webpackIgnore: true */
|
|
1835
|
+
"pixi"
|
|
1836
|
+
);
|
|
1837
|
+
_PIXI = mod.default || mod;
|
|
1838
|
+
return _PIXI;
|
|
1839
|
+
} catch {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
})();
|
|
1844
|
+
const result = await _pixiLoadPromise;
|
|
1845
|
+
_pixiLoadPromise = null;
|
|
1846
|
+
return result;
|
|
1847
|
+
}
|
|
1848
|
+
function resolveColor(color, gradients) {
|
|
1849
|
+
if (!color) return 0;
|
|
1850
|
+
const gradRef = color.match(/^url\(#(.+)\)$/);
|
|
1851
|
+
if (gradRef && gradients) {
|
|
1852
|
+
const grad = gradients.find((g) => g.id === gradRef[1]);
|
|
1853
|
+
if (grad && grad.stops.length > 0) {
|
|
1854
|
+
return grad.stops[0].color;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return color;
|
|
1858
|
+
}
|
|
1859
|
+
function resolveGradientFill(color, gradients, PIXI) {
|
|
1860
|
+
if (!color) return void 0;
|
|
1861
|
+
const gradRef = color.match(/^url\(#(.+)\)$/);
|
|
1862
|
+
if (!gradRef || !gradients) return color;
|
|
1863
|
+
const grad = gradients.find((g) => g.id === gradRef[1]);
|
|
1864
|
+
if (!grad || grad.stops.length === 0) return color;
|
|
1865
|
+
if (PIXI && PIXI.FillGradient) {
|
|
1866
|
+
try {
|
|
1867
|
+
if (grad.type === "linear") {
|
|
1868
|
+
const lg = grad;
|
|
1869
|
+
const fg = new PIXI.FillGradient({
|
|
1870
|
+
type: "linear",
|
|
1871
|
+
start: { x: lg.x1, y: lg.y1 },
|
|
1872
|
+
end: { x: lg.x2, y: lg.y2 },
|
|
1873
|
+
colorStops: lg.stops.map((s) => ({ offset: s.offset, color: s.color }))
|
|
1874
|
+
});
|
|
1875
|
+
return fg;
|
|
1876
|
+
}
|
|
1877
|
+
} catch {
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return grad.stops[0].color;
|
|
1881
|
+
}
|
|
1882
|
+
function parseSvgPath(d) {
|
|
1883
|
+
const commands = [];
|
|
1884
|
+
const re = /([MmLlHhVvCcSsQqTtAaZz])/;
|
|
1885
|
+
const tokens = d.split(re).filter((s) => s.trim().length > 0);
|
|
1886
|
+
let i = 0;
|
|
1887
|
+
let curX = 0, curY = 0;
|
|
1888
|
+
let startX = 0, startY = 0;
|
|
1889
|
+
while (i < tokens.length) {
|
|
1890
|
+
const cmd = tokens[i];
|
|
1891
|
+
i++;
|
|
1892
|
+
const nums = [];
|
|
1893
|
+
if (i < tokens.length && !re.test(tokens[i])) {
|
|
1894
|
+
const raw = tokens[i].trim().replace(/,/g, " ").replace(/-/g, " -").replace(/\s+/g, " ").trim();
|
|
1895
|
+
if (raw.length > 0) {
|
|
1896
|
+
nums.push(...raw.split(" ").filter((s) => s.length > 0).map(Number));
|
|
1897
|
+
}
|
|
1898
|
+
i++;
|
|
1899
|
+
}
|
|
1900
|
+
switch (cmd) {
|
|
1901
|
+
case "M":
|
|
1902
|
+
for (let j = 0; j < nums.length; j += 2) {
|
|
1903
|
+
curX = nums[j];
|
|
1904
|
+
curY = nums[j + 1];
|
|
1905
|
+
if (j === 0) {
|
|
1906
|
+
startX = curX;
|
|
1907
|
+
startY = curY;
|
|
1908
|
+
}
|
|
1909
|
+
commands.push({ cmd: "M", x: curX, y: curY });
|
|
1910
|
+
}
|
|
1911
|
+
break;
|
|
1912
|
+
case "m":
|
|
1913
|
+
for (let j = 0; j < nums.length; j += 2) {
|
|
1914
|
+
curX += nums[j];
|
|
1915
|
+
curY += nums[j + 1];
|
|
1916
|
+
if (j === 0) {
|
|
1917
|
+
startX = curX;
|
|
1918
|
+
startY = curY;
|
|
1919
|
+
}
|
|
1920
|
+
commands.push({ cmd: "M", x: curX, y: curY });
|
|
1921
|
+
}
|
|
1922
|
+
break;
|
|
1923
|
+
case "L":
|
|
1924
|
+
for (let j = 0; j < nums.length; j += 2) {
|
|
1925
|
+
curX = nums[j];
|
|
1926
|
+
curY = nums[j + 1];
|
|
1927
|
+
commands.push({ cmd: "L", x: curX, y: curY });
|
|
1928
|
+
}
|
|
1929
|
+
break;
|
|
1930
|
+
case "l":
|
|
1931
|
+
for (let j = 0; j < nums.length; j += 2) {
|
|
1932
|
+
curX += nums[j];
|
|
1933
|
+
curY += nums[j + 1];
|
|
1934
|
+
commands.push({ cmd: "L", x: curX, y: curY });
|
|
1935
|
+
}
|
|
1936
|
+
break;
|
|
1937
|
+
case "H":
|
|
1938
|
+
for (let j = 0; j < nums.length; j++) {
|
|
1939
|
+
curX = nums[j];
|
|
1940
|
+
commands.push({ cmd: "L", x: curX, y: curY });
|
|
1941
|
+
}
|
|
1942
|
+
break;
|
|
1943
|
+
case "h":
|
|
1944
|
+
for (let j = 0; j < nums.length; j++) {
|
|
1945
|
+
curX += nums[j];
|
|
1946
|
+
commands.push({ cmd: "L", x: curX, y: curY });
|
|
1947
|
+
}
|
|
1948
|
+
break;
|
|
1949
|
+
case "V":
|
|
1950
|
+
for (let j = 0; j < nums.length; j++) {
|
|
1951
|
+
curY = nums[j];
|
|
1952
|
+
commands.push({ cmd: "L", x: curX, y: curY });
|
|
1953
|
+
}
|
|
1954
|
+
break;
|
|
1955
|
+
case "v":
|
|
1956
|
+
for (let j = 0; j < nums.length; j++) {
|
|
1957
|
+
curY += nums[j];
|
|
1958
|
+
commands.push({ cmd: "L", x: curX, y: curY });
|
|
1959
|
+
}
|
|
1960
|
+
break;
|
|
1961
|
+
case "C":
|
|
1962
|
+
for (let j = 0; j < nums.length; j += 6) {
|
|
1963
|
+
commands.push({ cmd: "C", x1: nums[j], y1: nums[j + 1], x2: nums[j + 2], y2: nums[j + 3], x: nums[j + 4], y: nums[j + 5] });
|
|
1964
|
+
curX = nums[j + 4];
|
|
1965
|
+
curY = nums[j + 5];
|
|
1966
|
+
}
|
|
1967
|
+
break;
|
|
1968
|
+
case "c":
|
|
1969
|
+
for (let j = 0; j < nums.length; j += 6) {
|
|
1970
|
+
commands.push({ cmd: "C", x1: curX + nums[j], y1: curY + nums[j + 1], x2: curX + nums[j + 2], y2: curY + nums[j + 3], x: curX + nums[j + 4], y: curY + nums[j + 5] });
|
|
1971
|
+
curX += nums[j + 4];
|
|
1972
|
+
curY += nums[j + 5];
|
|
1973
|
+
}
|
|
1974
|
+
break;
|
|
1975
|
+
case "Q":
|
|
1976
|
+
for (let j = 0; j < nums.length; j += 4) {
|
|
1977
|
+
commands.push({ cmd: "Q", x1: nums[j], y1: nums[j + 1], x: nums[j + 2], y: nums[j + 3] });
|
|
1978
|
+
curX = nums[j + 2];
|
|
1979
|
+
curY = nums[j + 3];
|
|
1980
|
+
}
|
|
1981
|
+
break;
|
|
1982
|
+
case "q":
|
|
1983
|
+
for (let j = 0; j < nums.length; j += 4) {
|
|
1984
|
+
commands.push({ cmd: "Q", x1: curX + nums[j], y1: curY + nums[j + 1], x: curX + nums[j + 2], y: curY + nums[j + 3] });
|
|
1985
|
+
curX += nums[j + 2];
|
|
1986
|
+
curY += nums[j + 3];
|
|
1987
|
+
}
|
|
1988
|
+
break;
|
|
1989
|
+
case "S":
|
|
1990
|
+
case "s": {
|
|
1991
|
+
for (let j = 0; j < nums.length; j += 4) {
|
|
1992
|
+
const abs = cmd === "S";
|
|
1993
|
+
const x2 = abs ? nums[j] : curX + nums[j];
|
|
1994
|
+
const y2 = abs ? nums[j + 1] : curY + nums[j + 1];
|
|
1995
|
+
const x = abs ? nums[j + 2] : curX + nums[j + 2];
|
|
1996
|
+
const y = abs ? nums[j + 3] : curY + nums[j + 3];
|
|
1997
|
+
commands.push({ cmd: "C", x1: curX, y1: curY, x2, y2, x, y });
|
|
1998
|
+
curX = x;
|
|
1999
|
+
curY = y;
|
|
2000
|
+
}
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
case "T":
|
|
2004
|
+
case "t": {
|
|
2005
|
+
for (let j = 0; j < nums.length; j += 2) {
|
|
2006
|
+
const abs = cmd === "T";
|
|
2007
|
+
const x = abs ? nums[j] : curX + nums[j];
|
|
2008
|
+
const y = abs ? nums[j + 1] : curY + nums[j + 1];
|
|
2009
|
+
commands.push({ cmd: "Q", x1: curX, y1: curY, x, y });
|
|
2010
|
+
curX = x;
|
|
2011
|
+
curY = y;
|
|
2012
|
+
}
|
|
2013
|
+
break;
|
|
2014
|
+
}
|
|
2015
|
+
case "A":
|
|
2016
|
+
case "a": {
|
|
2017
|
+
for (let j = 0; j < nums.length; j += 7) {
|
|
2018
|
+
const abs = cmd === "A";
|
|
2019
|
+
const ex = abs ? nums[j + 5] : curX + nums[j + 5];
|
|
2020
|
+
const ey = abs ? nums[j + 6] : curY + nums[j + 6];
|
|
2021
|
+
commands.push({ cmd: "L", x: ex, y: ey });
|
|
2022
|
+
curX = ex;
|
|
2023
|
+
curY = ey;
|
|
2024
|
+
}
|
|
2025
|
+
break;
|
|
2026
|
+
}
|
|
2027
|
+
case "Z":
|
|
2028
|
+
case "z":
|
|
2029
|
+
commands.push({ cmd: "Z" });
|
|
2030
|
+
curX = startX;
|
|
2031
|
+
curY = startY;
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return commands;
|
|
2036
|
+
}
|
|
2037
|
+
function drawSvgPath(g, d) {
|
|
2038
|
+
const cmds = parseSvgPath(d);
|
|
2039
|
+
for (const c of cmds) {
|
|
2040
|
+
switch (c.cmd) {
|
|
2041
|
+
case "M":
|
|
2042
|
+
g.moveTo(c.x, c.y);
|
|
2043
|
+
break;
|
|
2044
|
+
case "L":
|
|
2045
|
+
g.lineTo(c.x, c.y);
|
|
2046
|
+
break;
|
|
2047
|
+
case "C":
|
|
2048
|
+
g.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);
|
|
2049
|
+
break;
|
|
2050
|
+
case "Q":
|
|
2051
|
+
g.quadraticCurveTo(c.x1, c.y1, c.x, c.y);
|
|
2052
|
+
break;
|
|
2053
|
+
case "Z":
|
|
2054
|
+
g.closePath();
|
|
2055
|
+
break;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function getMeta(obj) {
|
|
2060
|
+
return obj?.__vv;
|
|
2061
|
+
}
|
|
2062
|
+
function setMeta(obj, meta) {
|
|
2063
|
+
obj.__vv = meta;
|
|
2064
|
+
}
|
|
2065
|
+
function nodeSnapshot(node) {
|
|
2066
|
+
if (node.type === "group") {
|
|
2067
|
+
const { children, ...rest } = node;
|
|
2068
|
+
return JSON.stringify(rest);
|
|
2069
|
+
}
|
|
2070
|
+
return JSON.stringify(node);
|
|
2071
|
+
}
|
|
2072
|
+
function applyTransform(obj, t) {
|
|
2073
|
+
if (!t) {
|
|
2074
|
+
obj.position.set(0, 0);
|
|
2075
|
+
obj.rotation = 0;
|
|
2076
|
+
obj.scale.set(1, 1);
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
obj.position.set(t.x ?? 0, t.y ?? 0);
|
|
2080
|
+
obj.rotation = (t.rotation ?? 0) * Math.PI / 180;
|
|
2081
|
+
obj.scale.set(t.scaleX ?? 1, t.scaleY ?? 1);
|
|
2082
|
+
}
|
|
2083
|
+
function applyStyle(obj, s) {
|
|
2084
|
+
if (!obj) return;
|
|
2085
|
+
obj.alpha = s?.opacity ?? 1;
|
|
2086
|
+
obj.visible = s?.visible !== false;
|
|
2087
|
+
if (s?.cursor && obj.cursor !== void 0) {
|
|
2088
|
+
obj.cursor = s.cursor;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
function applyGraphicsStyle(g, style, gradients, PIXI) {
|
|
2092
|
+
const fill = style?.fill;
|
|
2093
|
+
const stroke = style?.stroke;
|
|
2094
|
+
const strokeWidth = style?.strokeWidth ?? 1;
|
|
2095
|
+
if (fill && fill !== "none") {
|
|
2096
|
+
const resolved = resolveGradientFill(fill, gradients, PIXI);
|
|
2097
|
+
if (typeof resolved === "object" && resolved !== null && resolved.constructor && resolved.constructor.name === "FillGradient") {
|
|
2098
|
+
g.fill(resolved);
|
|
2099
|
+
} else {
|
|
2100
|
+
const fillAlpha = style?.fillOpacity ?? 1;
|
|
2101
|
+
g.fill({ color: resolved, alpha: fillAlpha });
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (stroke && stroke !== "none") {
|
|
2105
|
+
const strokeAlpha = style?.strokeOpacity ?? 1;
|
|
2106
|
+
const strokeOpts = {
|
|
2107
|
+
width: strokeWidth,
|
|
2108
|
+
color: resolveColor(stroke, gradients),
|
|
2109
|
+
alpha: strokeAlpha
|
|
2110
|
+
};
|
|
2111
|
+
if (style?.strokeLinecap) strokeOpts.cap = mapLinecap(style.strokeLinecap);
|
|
2112
|
+
if (style?.strokeLinejoin) strokeOpts.join = mapLinejoin(style.strokeLinejoin);
|
|
2113
|
+
g.stroke(strokeOpts);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function mapLinecap(cap) {
|
|
2117
|
+
switch (cap) {
|
|
2118
|
+
case "round":
|
|
2119
|
+
return "round";
|
|
2120
|
+
case "square":
|
|
2121
|
+
return "square";
|
|
2122
|
+
default:
|
|
2123
|
+
return "butt";
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function mapLinejoin(join) {
|
|
2127
|
+
switch (join) {
|
|
2128
|
+
case "round":
|
|
2129
|
+
return "round";
|
|
2130
|
+
case "bevel":
|
|
2131
|
+
return "bevel";
|
|
2132
|
+
default:
|
|
2133
|
+
return "miter";
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
function createDisplayObject(node, ctx) {
|
|
2137
|
+
const PIXI = ctx.PIXI;
|
|
2138
|
+
let obj;
|
|
2139
|
+
switch (node.type) {
|
|
2140
|
+
case "rect": {
|
|
2141
|
+
const n = node;
|
|
2142
|
+
const g = new PIXI.Graphics();
|
|
2143
|
+
if (n.rx || n.ry) {
|
|
2144
|
+
g.roundRect(-n.width / 2, -n.height / 2, n.width, n.height, n.rx ?? n.ry ?? 0);
|
|
2145
|
+
} else {
|
|
2146
|
+
g.rect(-n.width / 2, -n.height / 2, n.width, n.height);
|
|
2147
|
+
}
|
|
2148
|
+
applyGraphicsStyle(g, n.style, ctx.gradients, PIXI);
|
|
2149
|
+
obj = g;
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
case "circle": {
|
|
2153
|
+
const n = node;
|
|
2154
|
+
const g = new PIXI.Graphics();
|
|
2155
|
+
g.circle(0, 0, n.radius);
|
|
2156
|
+
applyGraphicsStyle(g, n.style, ctx.gradients, PIXI);
|
|
2157
|
+
obj = g;
|
|
2158
|
+
break;
|
|
2159
|
+
}
|
|
2160
|
+
case "ellipse": {
|
|
2161
|
+
const n = node;
|
|
2162
|
+
const g = new PIXI.Graphics();
|
|
2163
|
+
g.ellipse(0, 0, n.rx, n.ry);
|
|
2164
|
+
applyGraphicsStyle(g, n.style, ctx.gradients, PIXI);
|
|
2165
|
+
obj = g;
|
|
2166
|
+
break;
|
|
2167
|
+
}
|
|
2168
|
+
case "line": {
|
|
2169
|
+
const n = node;
|
|
2170
|
+
const g = new PIXI.Graphics();
|
|
2171
|
+
g.moveTo(0, 0);
|
|
2172
|
+
g.lineTo(n.x2, n.y2);
|
|
2173
|
+
const style = { ...n.style ?? {}, fill: void 0 };
|
|
2174
|
+
applyGraphicsStyle(g, style, ctx.gradients, PIXI);
|
|
2175
|
+
if (!n.style?.stroke) {
|
|
2176
|
+
g.stroke({ width: n.style?.strokeWidth ?? 1, color: 16777215 });
|
|
2177
|
+
}
|
|
2178
|
+
obj = g;
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
case "polyline": {
|
|
2182
|
+
const n = node;
|
|
2183
|
+
const g = new PIXI.Graphics();
|
|
2184
|
+
if (n.points.length > 0) {
|
|
2185
|
+
g.moveTo(n.points[0].x, n.points[0].y);
|
|
2186
|
+
for (let i = 1; i < n.points.length; i++) {
|
|
2187
|
+
g.lineTo(n.points[i].x, n.points[i].y);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const style = { ...n.style ?? {}, fill: n.style?.fill ?? "none" };
|
|
2191
|
+
applyGraphicsStyle(g, style, ctx.gradients, PIXI);
|
|
2192
|
+
if (!n.style?.stroke) {
|
|
2193
|
+
g.stroke({ width: n.style?.strokeWidth ?? 1, color: 16777215 });
|
|
2194
|
+
}
|
|
2195
|
+
obj = g;
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
case "polygon": {
|
|
2199
|
+
const n = node;
|
|
2200
|
+
const g = new PIXI.Graphics();
|
|
2201
|
+
if (n.points.length > 0) {
|
|
2202
|
+
const flat = [];
|
|
2203
|
+
for (const p of n.points) {
|
|
2204
|
+
flat.push(p.x, p.y);
|
|
2205
|
+
}
|
|
2206
|
+
g.poly(flat, true);
|
|
2207
|
+
}
|
|
2208
|
+
applyGraphicsStyle(g, n.style, ctx.gradients, PIXI);
|
|
2209
|
+
obj = g;
|
|
2210
|
+
break;
|
|
2211
|
+
}
|
|
2212
|
+
case "path": {
|
|
2213
|
+
const n = node;
|
|
2214
|
+
const g = new PIXI.Graphics();
|
|
2215
|
+
drawSvgPath(g, n.d);
|
|
2216
|
+
applyGraphicsStyle(g, n.style, ctx.gradients, PIXI);
|
|
2217
|
+
obj = g;
|
|
2218
|
+
break;
|
|
2219
|
+
}
|
|
2220
|
+
case "text": {
|
|
2221
|
+
const n = node;
|
|
2222
|
+
const ts = n.style;
|
|
2223
|
+
const pixiStyle = {
|
|
2224
|
+
fontSize: ts?.fontSize ?? 16,
|
|
2225
|
+
fontFamily: ts?.fontFamily ?? "Arial",
|
|
2226
|
+
fontWeight: ts?.fontWeight != null ? String(ts.fontWeight) : "normal",
|
|
2227
|
+
fill: resolveColor(ts?.fill ?? "#ffffff", ctx.gradients),
|
|
2228
|
+
letterSpacing: ts?.letterSpacing ?? 0
|
|
2229
|
+
};
|
|
2230
|
+
if (ts?.stroke && ts.stroke !== "none") {
|
|
2231
|
+
pixiStyle.stroke = {
|
|
2232
|
+
color: resolveColor(ts.stroke, ctx.gradients),
|
|
2233
|
+
width: ts.strokeWidth ?? 1
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
const text = new PIXI.Text({ text: n.text, style: pixiStyle });
|
|
2237
|
+
const anchor = ts?.textAnchor ?? "start";
|
|
2238
|
+
switch (anchor) {
|
|
2239
|
+
case "middle":
|
|
2240
|
+
text.anchor.set(0.5, 0.5);
|
|
2241
|
+
break;
|
|
2242
|
+
case "end":
|
|
2243
|
+
text.anchor.set(1, 0.5);
|
|
2244
|
+
break;
|
|
2245
|
+
default:
|
|
2246
|
+
text.anchor.set(0, 0.5);
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
const baseline = ts?.dominantBaseline ?? "auto";
|
|
2250
|
+
switch (baseline) {
|
|
2251
|
+
case "middle":
|
|
2252
|
+
text.anchor.y = 0.5;
|
|
2253
|
+
break;
|
|
2254
|
+
case "hanging":
|
|
2255
|
+
text.anchor.y = 0;
|
|
2256
|
+
break;
|
|
2257
|
+
case "text-top":
|
|
2258
|
+
text.anchor.y = 0;
|
|
2259
|
+
break;
|
|
2260
|
+
default:
|
|
2261
|
+
text.anchor.y = 0.8;
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
obj = text;
|
|
2265
|
+
break;
|
|
2266
|
+
}
|
|
2267
|
+
case "image": {
|
|
2268
|
+
const n = node;
|
|
2269
|
+
let texture;
|
|
2270
|
+
if (ctx.textureCache.has(n.href)) {
|
|
2271
|
+
texture = ctx.textureCache.get(n.href);
|
|
2272
|
+
} else {
|
|
2273
|
+
try {
|
|
2274
|
+
texture = PIXI.Texture.from(n.href);
|
|
2275
|
+
ctx.textureCache.set(n.href, texture);
|
|
2276
|
+
} catch {
|
|
2277
|
+
const g = new PIXI.Graphics();
|
|
2278
|
+
g.rect(-n.width / 2, -n.height / 2, n.width, n.height);
|
|
2279
|
+
g.fill({ color: 3355443 });
|
|
2280
|
+
g.stroke({ width: 1, color: 6710886 });
|
|
2281
|
+
obj = g;
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
const sprite = new PIXI.Sprite(texture);
|
|
2286
|
+
sprite.width = n.width;
|
|
2287
|
+
sprite.height = n.height;
|
|
2288
|
+
sprite.anchor.set(0.5, 0.5);
|
|
2289
|
+
obj = sprite;
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
case "group": {
|
|
2293
|
+
const container = new PIXI.Container();
|
|
2294
|
+
const n = node;
|
|
2295
|
+
for (const child of n.children) {
|
|
2296
|
+
const childObj = createDisplayObject(child, ctx);
|
|
2297
|
+
if (childObj) {
|
|
2298
|
+
applyTransform(childObj, child.transform);
|
|
2299
|
+
applyStyle(childObj, child.style);
|
|
2300
|
+
setupInteraction(childObj, child, ctx);
|
|
2301
|
+
setMeta(childObj, { nodeId: child.id, nodeType: child.type, snapshot: nodeSnapshot(child) });
|
|
2302
|
+
ctx.nodeMap.set(child.id, childObj);
|
|
2303
|
+
container.addChild(childObj);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
obj = container;
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
case "sprite": {
|
|
2310
|
+
const n = node;
|
|
2311
|
+
let texture;
|
|
2312
|
+
if (ctx.textureCache.has(n.href)) {
|
|
2313
|
+
texture = ctx.textureCache.get(n.href);
|
|
2314
|
+
} else {
|
|
2315
|
+
try {
|
|
2316
|
+
texture = PIXI.Texture.from(n.href);
|
|
2317
|
+
ctx.textureCache.set(n.href, texture);
|
|
2318
|
+
} catch {
|
|
2319
|
+
const g = new PIXI.Graphics();
|
|
2320
|
+
g.rect(0, 0, n.frameWidth, n.frameHeight);
|
|
2321
|
+
g.fill({ color: 3355443 });
|
|
2322
|
+
obj = g;
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
const cols = n.columns ?? Math.floor(1e3 / n.frameWidth);
|
|
2327
|
+
const frame = n.frame ?? 0;
|
|
2328
|
+
const col = frame % cols;
|
|
2329
|
+
const row = Math.floor(frame / cols);
|
|
2330
|
+
try {
|
|
2331
|
+
const frameTexture = new PIXI.Texture({
|
|
2332
|
+
source: texture.source,
|
|
2333
|
+
frame: new PIXI.Rectangle(
|
|
2334
|
+
col * n.frameWidth,
|
|
2335
|
+
row * n.frameHeight,
|
|
2336
|
+
n.frameWidth,
|
|
2337
|
+
n.frameHeight
|
|
2338
|
+
)
|
|
2339
|
+
});
|
|
2340
|
+
const sprite = new PIXI.Sprite(frameTexture);
|
|
2341
|
+
obj = sprite;
|
|
2342
|
+
} catch {
|
|
2343
|
+
const sprite = new PIXI.Sprite(texture);
|
|
2344
|
+
sprite.width = n.frameWidth;
|
|
2345
|
+
sprite.height = n.frameHeight;
|
|
2346
|
+
obj = sprite;
|
|
2347
|
+
}
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
case "tilemap": {
|
|
2351
|
+
const n = node;
|
|
2352
|
+
const container = new PIXI.Container();
|
|
2353
|
+
let baseTexture;
|
|
2354
|
+
if (ctx.textureCache.has(n.href)) {
|
|
2355
|
+
baseTexture = ctx.textureCache.get(n.href);
|
|
2356
|
+
} else {
|
|
2357
|
+
try {
|
|
2358
|
+
baseTexture = PIXI.Texture.from(n.href);
|
|
2359
|
+
ctx.textureCache.set(n.href, baseTexture);
|
|
2360
|
+
} catch {
|
|
2361
|
+
obj = container;
|
|
2362
|
+
break;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
for (let row = 0; row < n.height; row++) {
|
|
2366
|
+
for (let col = 0; col < n.width; col++) {
|
|
2367
|
+
const tileIdx = n.data[row]?.[col];
|
|
2368
|
+
if (tileIdx == null || tileIdx < 0) continue;
|
|
2369
|
+
const srcCol = tileIdx % n.columns;
|
|
2370
|
+
const srcRow = Math.floor(tileIdx / n.columns);
|
|
2371
|
+
try {
|
|
2372
|
+
const tileTexture = new PIXI.Texture({
|
|
2373
|
+
source: baseTexture.source,
|
|
2374
|
+
frame: new PIXI.Rectangle(
|
|
2375
|
+
srcCol * n.tileWidth,
|
|
2376
|
+
srcRow * n.tileHeight,
|
|
2377
|
+
n.tileWidth,
|
|
2378
|
+
n.tileHeight
|
|
2379
|
+
)
|
|
2380
|
+
});
|
|
2381
|
+
const tileSprite = new PIXI.Sprite(tileTexture);
|
|
2382
|
+
tileSprite.position.set(col * n.tileWidth, row * n.tileHeight);
|
|
2383
|
+
container.addChild(tileSprite);
|
|
2384
|
+
} catch {
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
obj = container;
|
|
2389
|
+
break;
|
|
2390
|
+
}
|
|
2391
|
+
case "particles": {
|
|
2392
|
+
const n = node;
|
|
2393
|
+
const container = new PIXI.Container();
|
|
2394
|
+
const particles = n._particles ?? [];
|
|
2395
|
+
for (let i = 0; i < particles.length; i++) {
|
|
2396
|
+
const p = particles[i];
|
|
2397
|
+
const emitter = n.emitters[0];
|
|
2398
|
+
const fadeOut = emitter?.fadeOut !== false;
|
|
2399
|
+
const alpha = fadeOut ? Math.max(0, 1 - p.age / p.lifetime) : 1;
|
|
2400
|
+
const shape = emitter?.shape ?? "circle";
|
|
2401
|
+
const g = new PIXI.Graphics();
|
|
2402
|
+
if (shape === "square") {
|
|
2403
|
+
g.rect(-p.size / 2, -p.size / 2, p.size, p.size);
|
|
2404
|
+
} else {
|
|
2405
|
+
g.circle(0, 0, p.size / 2);
|
|
2406
|
+
}
|
|
2407
|
+
g.fill({ color: p.color, alpha });
|
|
2408
|
+
g.position.set(p.x, p.y);
|
|
2409
|
+
container.addChild(g);
|
|
2410
|
+
}
|
|
2411
|
+
obj = container;
|
|
2412
|
+
break;
|
|
2413
|
+
}
|
|
2414
|
+
default:
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
return obj;
|
|
2418
|
+
}
|
|
2419
|
+
function setupInteraction(obj, node, ctx) {
|
|
2420
|
+
if (!node.interactive) {
|
|
2421
|
+
obj.eventMode = "auto";
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
obj.eventMode = "static";
|
|
2425
|
+
if (node.style?.cursor) {
|
|
2426
|
+
obj.cursor = node.style.cursor;
|
|
2427
|
+
} else {
|
|
2428
|
+
obj.cursor = "pointer";
|
|
2429
|
+
}
|
|
2430
|
+
obj.removeAllListeners?.();
|
|
2431
|
+
if (ctx.onNodeClick) {
|
|
2432
|
+
obj.on("pointerdown", (e) => {
|
|
2433
|
+
const global = e.global || e.data?.global;
|
|
2434
|
+
if (global) {
|
|
2435
|
+
ctx.onNodeClick(node.id, { x: global.x, y: global.y });
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
if (ctx.onNodeHover) {
|
|
2440
|
+
obj.on("pointerenter", () => ctx.onNodeHover(node.id));
|
|
2441
|
+
obj.on("pointerleave", () => ctx.onNodeHover(null));
|
|
2442
|
+
}
|
|
2443
|
+
if (ctx.onNodeDragStart || ctx.onNodeDrag || ctx.onNodeDragEnd) {
|
|
2444
|
+
let dragging = false;
|
|
2445
|
+
obj.on("pointerdown", (e) => {
|
|
2446
|
+
dragging = true;
|
|
2447
|
+
const global = e.global || e.data?.global;
|
|
2448
|
+
if (global && ctx.onNodeDragStart) {
|
|
2449
|
+
ctx.onNodeDragStart(node.id, { x: global.x, y: global.y });
|
|
2450
|
+
}
|
|
2451
|
+
e.stopPropagation?.();
|
|
2452
|
+
});
|
|
2453
|
+
obj.on("globalpointermove", (e) => {
|
|
2454
|
+
if (!dragging) return;
|
|
2455
|
+
const global = e.global || e.data?.global;
|
|
2456
|
+
if (global && ctx.onNodeDrag) {
|
|
2457
|
+
ctx.onNodeDrag(node.id, { x: global.x, y: global.y });
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
obj.on("pointerup", (e) => {
|
|
2461
|
+
if (!dragging) return;
|
|
2462
|
+
dragging = false;
|
|
2463
|
+
const global = e.global || e.data?.global;
|
|
2464
|
+
if (global && ctx.onNodeDragEnd) {
|
|
2465
|
+
ctx.onNodeDragEnd(node.id, { x: global.x, y: global.y });
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
obj.on("pointerupoutside", (e) => {
|
|
2469
|
+
if (!dragging) return;
|
|
2470
|
+
dragging = false;
|
|
2471
|
+
const global = e.global || e.data?.global;
|
|
2472
|
+
if (global && ctx.onNodeDragEnd) {
|
|
2473
|
+
ctx.onNodeDragEnd(node.id, { x: global.x, y: global.y });
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function syncNode(node, parent, ctx) {
|
|
2479
|
+
const existing = ctx.nodeMap.get(node.id);
|
|
2480
|
+
const snap = nodeSnapshot(node);
|
|
2481
|
+
if (existing) {
|
|
2482
|
+
const meta = getMeta(existing);
|
|
2483
|
+
if (meta && meta.snapshot === snap && node.type !== "group") {
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (meta && meta.nodeType !== node.type) {
|
|
2487
|
+
removeDisplayObject(existing, node.id, ctx);
|
|
2488
|
+
} else if (node.type === "group") {
|
|
2489
|
+
applyTransform(existing, node.transform);
|
|
2490
|
+
applyStyle(existing, node.style);
|
|
2491
|
+
setMeta(existing, { nodeId: node.id, nodeType: node.type, snapshot: snap });
|
|
2492
|
+
syncGroupChildren(node, existing, ctx);
|
|
2493
|
+
return;
|
|
2494
|
+
} else {
|
|
2495
|
+
removeDisplayObject(existing, node.id, ctx);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
const obj = createDisplayObject(node, ctx);
|
|
2499
|
+
if (!obj) return;
|
|
2500
|
+
applyTransform(obj, node.transform);
|
|
2501
|
+
applyStyle(obj, node.style);
|
|
2502
|
+
setupInteraction(obj, node, ctx);
|
|
2503
|
+
setMeta(obj, { nodeId: node.id, nodeType: node.type, snapshot: snap });
|
|
2504
|
+
ctx.nodeMap.set(node.id, obj);
|
|
2505
|
+
parent.addChild(obj);
|
|
2506
|
+
}
|
|
2507
|
+
function syncGroupChildren(groupNode, container, ctx) {
|
|
2508
|
+
const childIds = new Set(groupNode.children.map((c) => c.id));
|
|
2509
|
+
const toRemove = [];
|
|
2510
|
+
for (let i = container.children.length - 1; i >= 0; i--) {
|
|
2511
|
+
const child = container.children[i];
|
|
2512
|
+
const meta = getMeta(child);
|
|
2513
|
+
if (meta && !childIds.has(meta.nodeId)) {
|
|
2514
|
+
toRemove.push(meta.nodeId);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
for (const id of toRemove) {
|
|
2518
|
+
removeDisplayObject(ctx.nodeMap.get(id), id, ctx);
|
|
2519
|
+
}
|
|
2520
|
+
for (let i = 0; i < groupNode.children.length; i++) {
|
|
2521
|
+
const childNode = groupNode.children[i];
|
|
2522
|
+
syncNode(childNode, container, ctx);
|
|
2523
|
+
const childObj = ctx.nodeMap.get(childNode.id);
|
|
2524
|
+
if (childObj && childObj.parent === container) {
|
|
2525
|
+
const currentIndex = container.getChildIndex(childObj);
|
|
2526
|
+
if (currentIndex !== i && i < container.children.length) {
|
|
2527
|
+
container.setChildIndex(childObj, Math.min(i, container.children.length - 1));
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
function removeDisplayObject(obj, nodeId, ctx) {
|
|
2533
|
+
if (!obj) return;
|
|
2534
|
+
if (obj.children) {
|
|
2535
|
+
for (let i = obj.children.length - 1; i >= 0; i--) {
|
|
2536
|
+
const child = obj.children[i];
|
|
2537
|
+
const meta = getMeta(child);
|
|
2538
|
+
if (meta) {
|
|
2539
|
+
removeDisplayObject(child, meta.nodeId, ctx);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
obj.removeAllListeners?.();
|
|
2544
|
+
obj.removeFromParent?.();
|
|
2545
|
+
obj.destroy?.({ children: true });
|
|
2546
|
+
ctx.nodeMap.delete(nodeId);
|
|
2547
|
+
}
|
|
2548
|
+
function drawSelectionOverlays(ctx, selectedIds) {
|
|
2549
|
+
const layer = ctx.selectionLayer;
|
|
2550
|
+
if (!layer) return;
|
|
2551
|
+
while (layer.children.length > 0) {
|
|
2552
|
+
const child = layer.children[0];
|
|
2553
|
+
child.removeFromParent();
|
|
2554
|
+
child.destroy?.();
|
|
2555
|
+
}
|
|
2556
|
+
if (!selectedIds || selectedIds.length === 0) return;
|
|
2557
|
+
const PIXI = ctx.PIXI;
|
|
2558
|
+
for (const nodeId of selectedIds) {
|
|
2559
|
+
const obj = ctx.nodeMap.get(nodeId);
|
|
2560
|
+
if (!obj) continue;
|
|
2561
|
+
try {
|
|
2562
|
+
const bounds = obj.getBounds();
|
|
2563
|
+
if (!bounds || bounds.width === 0 || bounds.height === 0) continue;
|
|
2564
|
+
const g = new PIXI.Graphics();
|
|
2565
|
+
const pad = 4;
|
|
2566
|
+
const x = bounds.x - pad;
|
|
2567
|
+
const y = bounds.y - pad;
|
|
2568
|
+
const w = bounds.width + pad * 2;
|
|
2569
|
+
const hh = bounds.height + pad * 2;
|
|
2570
|
+
drawDashedRect(g, x, y, w, hh, 4, 2);
|
|
2571
|
+
g.stroke({ width: 2, color: 6514417 });
|
|
2572
|
+
layer.addChild(g);
|
|
2573
|
+
} catch {
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function drawDashedRect(g, x, y, w, h7, dashLen, gapLen) {
|
|
2578
|
+
const edges = [
|
|
2579
|
+
{ sx: x, sy: y, ex: x + w, ey: y },
|
|
2580
|
+
// top
|
|
2581
|
+
{ sx: x + w, sy: y, ex: x + w, ey: y + h7 },
|
|
2582
|
+
// right
|
|
2583
|
+
{ sx: x + w, sy: y + h7, ex: x, ey: y + h7 },
|
|
2584
|
+
// bottom
|
|
2585
|
+
{ sx: x, sy: y + h7, ex: x, ey: y }
|
|
2586
|
+
// left
|
|
2587
|
+
];
|
|
2588
|
+
for (const edge of edges) {
|
|
2589
|
+
const dx = edge.ex - edge.sx;
|
|
2590
|
+
const dy = edge.ey - edge.sy;
|
|
2591
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2592
|
+
if (length === 0) continue;
|
|
2593
|
+
const ux = dx / length;
|
|
2594
|
+
const uy = dy / length;
|
|
2595
|
+
let dist2 = 0;
|
|
2596
|
+
let drawing = true;
|
|
2597
|
+
while (dist2 < length) {
|
|
2598
|
+
const segLen = drawing ? dashLen : gapLen;
|
|
2599
|
+
const endDist = Math.min(dist2 + segLen, length);
|
|
2600
|
+
if (drawing) {
|
|
2601
|
+
g.moveTo(edge.sx + ux * dist2, edge.sy + uy * dist2);
|
|
2602
|
+
g.lineTo(edge.sx + ux * endDist, edge.sy + uy * endDist);
|
|
2603
|
+
}
|
|
2604
|
+
dist2 = endDist;
|
|
2605
|
+
drawing = !drawing;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
function drawDebugLabels(ctx, rootNode) {
|
|
2610
|
+
const layer = ctx.debugLayer;
|
|
2611
|
+
if (!layer) return;
|
|
2612
|
+
while (layer.children.length > 0) {
|
|
2613
|
+
const child = layer.children[0];
|
|
2614
|
+
child.removeFromParent();
|
|
2615
|
+
child.destroy?.();
|
|
2616
|
+
}
|
|
2617
|
+
const PIXI = ctx.PIXI;
|
|
2618
|
+
function addLabel(node) {
|
|
2619
|
+
const obj = ctx.nodeMap.get(node.id);
|
|
2620
|
+
if (!obj) return;
|
|
2621
|
+
try {
|
|
2622
|
+
const bounds = obj.getBounds();
|
|
2623
|
+
const label = new PIXI.Text({
|
|
2624
|
+
text: node.id,
|
|
2625
|
+
style: {
|
|
2626
|
+
fontSize: 10,
|
|
2627
|
+
fill: "#94a3b8",
|
|
2628
|
+
fontFamily: "monospace"
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
label.anchor.set(0.5, 1);
|
|
2632
|
+
label.position.set(
|
|
2633
|
+
bounds.x + bounds.width / 2,
|
|
2634
|
+
bounds.y - 4
|
|
2635
|
+
);
|
|
2636
|
+
layer.addChild(label);
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
if (node.type === "group") {
|
|
2640
|
+
for (const child of node.children) {
|
|
2641
|
+
addLabel(child);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
for (const child of rootNode.children) {
|
|
2646
|
+
addLabel(child);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
function applyTweens(rootNode, ctx, now) {
|
|
2650
|
+
function processTween(node) {
|
|
2651
|
+
if (!node.tween || !node.tween.startedAt) {
|
|
2652
|
+
if (node.type === "group") {
|
|
2653
|
+
for (const child of node.children) {
|
|
2654
|
+
processTween(child);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
const value = interpolateTween(node.tween, now);
|
|
2660
|
+
if (value === null) {
|
|
2661
|
+
if (node.type === "group") {
|
|
2662
|
+
for (const child of node.children) {
|
|
2663
|
+
processTween(child);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
const obj = ctx.nodeMap.get(node.id);
|
|
2669
|
+
if (!obj) return;
|
|
2670
|
+
const prop = node.tween.property;
|
|
2671
|
+
if (prop === "transform.x") {
|
|
2672
|
+
obj.position.x = value;
|
|
2673
|
+
} else if (prop === "transform.y") {
|
|
2674
|
+
obj.position.y = value;
|
|
2675
|
+
} else if (prop === "transform.rotation") {
|
|
2676
|
+
obj.rotation = value * Math.PI / 180;
|
|
2677
|
+
} else if (prop === "transform.scaleX") {
|
|
2678
|
+
obj.scale.x = value;
|
|
2679
|
+
} else if (prop === "transform.scaleY") {
|
|
2680
|
+
obj.scale.y = value;
|
|
2681
|
+
} else if (prop === "style.opacity") {
|
|
2682
|
+
obj.alpha = value;
|
|
2683
|
+
}
|
|
2684
|
+
if (node.type === "group") {
|
|
2685
|
+
for (const child of node.children) {
|
|
2686
|
+
processTween(child);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
for (const child of rootNode.children) {
|
|
2691
|
+
processTween(child);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
function tickParticlesInternal(node, dt) {
|
|
2695
|
+
const particles = [...node._particles ?? []];
|
|
2696
|
+
const maxParticles = node.maxParticles ?? 200;
|
|
2697
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
2698
|
+
const p = particles[i];
|
|
2699
|
+
p.age += dt;
|
|
2700
|
+
if (p.age >= p.lifetime) {
|
|
2701
|
+
particles.splice(i, 1);
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
const gravity = node.emitters[0]?.gravity ?? 0;
|
|
2705
|
+
p.vy += gravity * (dt / 1e3);
|
|
2706
|
+
p.x += p.vx * (dt / 1e3);
|
|
2707
|
+
p.y += p.vy * (dt / 1e3);
|
|
2708
|
+
}
|
|
2709
|
+
for (const emitter of node.emitters) {
|
|
2710
|
+
const count = Math.floor(emitter.rate * (dt / 1e3));
|
|
2711
|
+
for (let i = 0; i < count && particles.length < maxParticles; i++) {
|
|
2712
|
+
const angle = emitter.direction.min + Math.random() * (emitter.direction.max - emitter.direction.min);
|
|
2713
|
+
const speed = emitter.speed.min + Math.random() * (emitter.speed.max - emitter.speed.min);
|
|
2714
|
+
const rad = angle * Math.PI / 180;
|
|
2715
|
+
const colors = emitter.color ? Array.isArray(emitter.color) ? emitter.color : [emitter.color] : ["#ffffff"];
|
|
2716
|
+
const size = emitter.size ? emitter.size.min + Math.random() * (emitter.size.max - emitter.size.min) : 4;
|
|
2717
|
+
particles.push({
|
|
2718
|
+
x: emitter.x,
|
|
2719
|
+
y: emitter.y,
|
|
2720
|
+
vx: Math.cos(rad) * speed,
|
|
2721
|
+
vy: Math.sin(rad) * speed,
|
|
2722
|
+
age: 0,
|
|
2723
|
+
lifetime: emitter.lifetime,
|
|
2724
|
+
size,
|
|
2725
|
+
color: colors[Math.floor(Math.random() * colors.length)]
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
return particles;
|
|
2730
|
+
}
|
|
2731
|
+
function PixiSceneRenderer(props) {
|
|
2732
|
+
const React4 = getReact4();
|
|
2733
|
+
const {
|
|
2734
|
+
scene,
|
|
2735
|
+
width = scene.width ?? 800,
|
|
2736
|
+
height = scene.height ?? 600,
|
|
2737
|
+
className,
|
|
2738
|
+
style: containerStyle,
|
|
2739
|
+
onNodeClick,
|
|
2740
|
+
onNodeHover,
|
|
2741
|
+
onNodeDragStart,
|
|
2742
|
+
onNodeDrag,
|
|
2743
|
+
onNodeDragEnd,
|
|
2744
|
+
onViewportClick,
|
|
2745
|
+
onViewportPan,
|
|
2746
|
+
onViewportZoom,
|
|
2747
|
+
selectedNodeIds,
|
|
2748
|
+
debug
|
|
2749
|
+
} = props;
|
|
2750
|
+
const containerRef = React4.useRef(null);
|
|
2751
|
+
const appRef = React4.useRef(null);
|
|
2752
|
+
const ctxRef = React4.useRef(null);
|
|
2753
|
+
const sceneRef = React4.useRef(scene);
|
|
2754
|
+
const propsRef = React4.useRef(props);
|
|
2755
|
+
const initializingRef = React4.useRef(false);
|
|
2756
|
+
const initializedRef = React4.useRef(false);
|
|
2757
|
+
const pixiAvailableRef = React4.useRef(null);
|
|
2758
|
+
const [, forceUpdate] = React4.useState(0);
|
|
2759
|
+
sceneRef.current = scene;
|
|
2760
|
+
propsRef.current = props;
|
|
2761
|
+
React4.useEffect(() => {
|
|
2762
|
+
if (initializingRef.current || initializedRef.current) return;
|
|
2763
|
+
initializingRef.current = true;
|
|
2764
|
+
let cancelled = false;
|
|
2765
|
+
(async () => {
|
|
2766
|
+
const PIXI = await loadPixi();
|
|
2767
|
+
if (cancelled) return;
|
|
2768
|
+
if (!PIXI) {
|
|
2769
|
+
pixiAvailableRef.current = false;
|
|
2770
|
+
initializingRef.current = false;
|
|
2771
|
+
forceUpdate((n) => n + 1);
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
pixiAvailableRef.current = true;
|
|
2775
|
+
const container = containerRef.current;
|
|
2776
|
+
if (!container || cancelled) {
|
|
2777
|
+
initializingRef.current = false;
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const app = new PIXI.Application();
|
|
2781
|
+
try {
|
|
2782
|
+
await app.init({
|
|
2783
|
+
width,
|
|
2784
|
+
height,
|
|
2785
|
+
background: scene.background ?? "#1a1a2e",
|
|
2786
|
+
antialias: true,
|
|
2787
|
+
resolution: window.devicePixelRatio || 1,
|
|
2788
|
+
autoDensity: true
|
|
2789
|
+
});
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
pixiAvailableRef.current = false;
|
|
2792
|
+
initializingRef.current = false;
|
|
2793
|
+
forceUpdate((n) => n + 1);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
if (cancelled) {
|
|
2797
|
+
app.destroy(true);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const canvas = app.canvas;
|
|
2801
|
+
if (canvas) {
|
|
2802
|
+
canvas.style.display = "block";
|
|
2803
|
+
container.appendChild(canvas);
|
|
2804
|
+
}
|
|
2805
|
+
const rootContainer = new PIXI.Container();
|
|
2806
|
+
const selectionLayer = new PIXI.Container();
|
|
2807
|
+
const debugLayer = new PIXI.Container();
|
|
2808
|
+
app.stage.addChild(rootContainer);
|
|
2809
|
+
app.stage.addChild(selectionLayer);
|
|
2810
|
+
app.stage.addChild(debugLayer);
|
|
2811
|
+
app.stage.eventMode = "static";
|
|
2812
|
+
app.stage.hitArea = new PIXI.Rectangle(0, 0, width, height);
|
|
2813
|
+
const ctx = {
|
|
2814
|
+
PIXI,
|
|
2815
|
+
app,
|
|
2816
|
+
gradients: scene.gradients,
|
|
2817
|
+
onNodeClick,
|
|
2818
|
+
onNodeHover,
|
|
2819
|
+
onNodeDragStart,
|
|
2820
|
+
onNodeDrag,
|
|
2821
|
+
onNodeDragEnd,
|
|
2822
|
+
selectedNodeIds,
|
|
2823
|
+
debug,
|
|
2824
|
+
nodeMap: /* @__PURE__ */ new Map(),
|
|
2825
|
+
textureCache: /* @__PURE__ */ new Map(),
|
|
2826
|
+
selectionLayer,
|
|
2827
|
+
debugLayer
|
|
2828
|
+
};
|
|
2829
|
+
appRef.current = app;
|
|
2830
|
+
ctxRef.current = ctx;
|
|
2831
|
+
syncScene(sceneRef.current, rootContainer, ctx, PIXI, width, height);
|
|
2832
|
+
setupViewportInteraction(app, canvas, PIXI, width, height);
|
|
2833
|
+
app.ticker.add((ticker) => {
|
|
2834
|
+
const dt = ticker.deltaMS ?? ticker.deltaTime * (1e3 / 60);
|
|
2835
|
+
const now = performance.now();
|
|
2836
|
+
const currentScene = sceneRef.current;
|
|
2837
|
+
const currentCtx = ctxRef.current;
|
|
2838
|
+
if (!currentCtx) return;
|
|
2839
|
+
applyTweens(currentScene.root, currentCtx, now);
|
|
2840
|
+
tickAllParticles(currentScene.root, currentCtx, dt);
|
|
2841
|
+
});
|
|
2842
|
+
initializedRef.current = true;
|
|
2843
|
+
initializingRef.current = false;
|
|
2844
|
+
forceUpdate((n) => n + 1);
|
|
2845
|
+
})();
|
|
2846
|
+
return () => {
|
|
2847
|
+
cancelled = true;
|
|
2848
|
+
};
|
|
2849
|
+
}, []);
|
|
2850
|
+
const viewportStateRef = React4.useRef({
|
|
2851
|
+
isPanning: false,
|
|
2852
|
+
panStartX: 0,
|
|
2853
|
+
panStartY: 0
|
|
2854
|
+
});
|
|
2855
|
+
function setupViewportInteraction(app, canvas, PIXI, w, h7) {
|
|
2856
|
+
if (!canvas) return;
|
|
2857
|
+
canvas.addEventListener("wheel", (e) => {
|
|
2858
|
+
e.preventDefault();
|
|
2859
|
+
const currentProps = propsRef.current;
|
|
2860
|
+
if (!currentProps.onViewportZoom) return;
|
|
2861
|
+
const rect = canvas.getBoundingClientRect();
|
|
2862
|
+
const cx = e.clientX - rect.left;
|
|
2863
|
+
const cy = e.clientY - rect.top;
|
|
2864
|
+
const currentScene = sceneRef.current;
|
|
2865
|
+
const camera = currentScene.camera ?? { x: w / 2, y: h7 / 2, zoom: 1 };
|
|
2866
|
+
const zoom = camera.zoom || 1;
|
|
2867
|
+
const factor = e.deltaY > 0 ? 0.9 : 1.1;
|
|
2868
|
+
const worldX = camera.x + (cx - w / 2) / zoom;
|
|
2869
|
+
const worldY = camera.y + (cy - h7 / 2) / zoom;
|
|
2870
|
+
currentProps.onViewportZoom(zoom * factor, { x: worldX, y: worldY });
|
|
2871
|
+
}, { passive: false });
|
|
2872
|
+
canvas.addEventListener("pointerdown", (e) => {
|
|
2873
|
+
const currentProps = propsRef.current;
|
|
2874
|
+
if (currentProps.onViewportPan) {
|
|
2875
|
+
viewportStateRef.current.isPanning = true;
|
|
2876
|
+
viewportStateRef.current.panStartX = e.clientX;
|
|
2877
|
+
viewportStateRef.current.panStartY = e.clientY;
|
|
2878
|
+
}
|
|
2879
|
+
if (currentProps.onViewportClick) {
|
|
2880
|
+
const rect = canvas.getBoundingClientRect();
|
|
2881
|
+
const currentScene = sceneRef.current;
|
|
2882
|
+
const camera = currentScene.camera ?? { x: w / 2, y: h7 / 2, zoom: 1 };
|
|
2883
|
+
const zoom = camera.zoom || 1;
|
|
2884
|
+
const cx = e.clientX - rect.left;
|
|
2885
|
+
const cy = e.clientY - rect.top;
|
|
2886
|
+
const worldX = camera.x + (cx - w / 2) / zoom;
|
|
2887
|
+
const worldY = camera.y + (cy - h7 / 2) / zoom;
|
|
2888
|
+
currentProps.onViewportClick({ x: worldX, y: worldY });
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
canvas.addEventListener("pointermove", (e) => {
|
|
2892
|
+
if (!viewportStateRef.current.isPanning) return;
|
|
2893
|
+
const currentProps = propsRef.current;
|
|
2894
|
+
if (!currentProps.onViewportPan) return;
|
|
2895
|
+
const currentScene = sceneRef.current;
|
|
2896
|
+
const camera = currentScene.camera ?? { x: w / 2, y: h7 / 2, zoom: 1 };
|
|
2897
|
+
const zoom = camera.zoom || 1;
|
|
2898
|
+
const dx = (e.clientX - viewportStateRef.current.panStartX) / zoom;
|
|
2899
|
+
const dy = (e.clientY - viewportStateRef.current.panStartY) / zoom;
|
|
2900
|
+
viewportStateRef.current.panStartX = e.clientX;
|
|
2901
|
+
viewportStateRef.current.panStartY = e.clientY;
|
|
2902
|
+
currentProps.onViewportPan({ x: -dx, y: -dy });
|
|
2903
|
+
});
|
|
2904
|
+
canvas.addEventListener("pointerup", () => {
|
|
2905
|
+
viewportStateRef.current.isPanning = false;
|
|
2906
|
+
});
|
|
2907
|
+
canvas.addEventListener("pointerleave", () => {
|
|
2908
|
+
viewportStateRef.current.isPanning = false;
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
function syncScene(scene2, rootContainer, ctx, PIXI, w, h7) {
|
|
2912
|
+
ctx.gradients = scene2.gradients;
|
|
2913
|
+
ctx.onNodeClick = propsRef.current.onNodeClick;
|
|
2914
|
+
ctx.onNodeHover = propsRef.current.onNodeHover;
|
|
2915
|
+
ctx.onNodeDragStart = propsRef.current.onNodeDragStart;
|
|
2916
|
+
ctx.onNodeDrag = propsRef.current.onNodeDrag;
|
|
2917
|
+
ctx.onNodeDragEnd = propsRef.current.onNodeDragEnd;
|
|
2918
|
+
ctx.selectedNodeIds = propsRef.current.selectedNodeIds;
|
|
2919
|
+
ctx.debug = propsRef.current.debug;
|
|
2920
|
+
const camera = scene2.camera ?? { x: w / 2, y: h7 / 2, zoom: 1 };
|
|
2921
|
+
const zoom = camera.zoom || 1;
|
|
2922
|
+
rootContainer.position.set(
|
|
2923
|
+
w / 2 - camera.x * zoom,
|
|
2924
|
+
h7 / 2 - camera.y * zoom
|
|
2925
|
+
);
|
|
2926
|
+
rootContainer.scale.set(zoom, zoom);
|
|
2927
|
+
if (camera.rotation) {
|
|
2928
|
+
rootContainer.rotation = camera.rotation * Math.PI / 180;
|
|
2929
|
+
rootContainer.pivot.set(camera.x, camera.y);
|
|
2930
|
+
rootContainer.position.set(w / 2, h7 / 2);
|
|
2931
|
+
}
|
|
2932
|
+
syncGroupChildren(scene2.root, rootContainer, ctx);
|
|
2933
|
+
drawSelectionOverlays(ctx, propsRef.current.selectedNodeIds);
|
|
2934
|
+
if (propsRef.current.debug) {
|
|
2935
|
+
drawDebugLabels(ctx, scene2.root);
|
|
2936
|
+
} else if (ctx.debugLayer) {
|
|
2937
|
+
while (ctx.debugLayer.children.length > 0) {
|
|
2938
|
+
const child = ctx.debugLayer.children[0];
|
|
2939
|
+
child.removeFromParent();
|
|
2940
|
+
child.destroy?.();
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
function tickAllParticles(rootNode, ctx, dt) {
|
|
2945
|
+
function processNode(node) {
|
|
2946
|
+
if (node.type === "particles") {
|
|
2947
|
+
const pNode = node;
|
|
2948
|
+
const particles = tickParticlesInternal(pNode, dt);
|
|
2949
|
+
pNode._particles = particles;
|
|
2950
|
+
const obj = ctx.nodeMap.get(node.id);
|
|
2951
|
+
if (obj) {
|
|
2952
|
+
while (obj.children?.length > 0) {
|
|
2953
|
+
const child = obj.children[0];
|
|
2954
|
+
child.removeFromParent();
|
|
2955
|
+
child.destroy?.();
|
|
2956
|
+
}
|
|
2957
|
+
const PIXI = ctx.PIXI;
|
|
2958
|
+
for (const p of particles) {
|
|
2959
|
+
const emitter = pNode.emitters[0];
|
|
2960
|
+
const fadeOut = emitter?.fadeOut !== false;
|
|
2961
|
+
const alpha = fadeOut ? Math.max(0, 1 - p.age / p.lifetime) : 1;
|
|
2962
|
+
const shape = emitter?.shape ?? "circle";
|
|
2963
|
+
const g = new PIXI.Graphics();
|
|
2964
|
+
if (shape === "square") {
|
|
2965
|
+
g.rect(-p.size / 2, -p.size / 2, p.size, p.size);
|
|
2966
|
+
} else {
|
|
2967
|
+
g.circle(0, 0, p.size / 2);
|
|
2968
|
+
}
|
|
2969
|
+
g.fill({ color: p.color, alpha });
|
|
2970
|
+
g.position.set(p.x, p.y);
|
|
2971
|
+
obj.addChild(g);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
if (node.type === "group") {
|
|
2976
|
+
for (const child of node.children) {
|
|
2977
|
+
processNode(child);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
for (const child of rootNode.children) {
|
|
2982
|
+
processNode(child);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
React4.useEffect(() => {
|
|
2986
|
+
if (!initializedRef.current || !appRef.current || !ctxRef.current) return;
|
|
2987
|
+
const app = appRef.current;
|
|
2988
|
+
const ctx = ctxRef.current;
|
|
2989
|
+
const PIXI = ctx.PIXI;
|
|
2990
|
+
const rootContainer = app.stage.children[0];
|
|
2991
|
+
if (!rootContainer) return;
|
|
2992
|
+
syncScene(scene, rootContainer, ctx, PIXI, width, height);
|
|
2993
|
+
}, [scene, width, height, selectedNodeIds, debug, onNodeClick, onNodeHover, onNodeDragStart, onNodeDrag, onNodeDragEnd]);
|
|
2994
|
+
React4.useEffect(() => {
|
|
2995
|
+
if (!appRef.current) return;
|
|
2996
|
+
const app = appRef.current;
|
|
2997
|
+
try {
|
|
2998
|
+
app.renderer.resize(width, height);
|
|
2999
|
+
const PIXI = ctxRef.current?.PIXI;
|
|
3000
|
+
if (PIXI) {
|
|
3001
|
+
app.stage.hitArea = new PIXI.Rectangle(0, 0, width, height);
|
|
3002
|
+
}
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
}, [width, height]);
|
|
3006
|
+
React4.useEffect(() => {
|
|
3007
|
+
if (!appRef.current) return;
|
|
3008
|
+
try {
|
|
3009
|
+
appRef.current.renderer.background.color = scene.background ?? "#1a1a2e";
|
|
3010
|
+
} catch {
|
|
3011
|
+
}
|
|
3012
|
+
}, [scene.background]);
|
|
3013
|
+
React4.useEffect(() => {
|
|
3014
|
+
return () => {
|
|
3015
|
+
const app = appRef.current;
|
|
3016
|
+
if (app) {
|
|
3017
|
+
try {
|
|
3018
|
+
app.destroy(true, { children: true, texture: false, baseTexture: false });
|
|
3019
|
+
} catch {
|
|
3020
|
+
}
|
|
3021
|
+
appRef.current = null;
|
|
3022
|
+
}
|
|
3023
|
+
const ctx = ctxRef.current;
|
|
3024
|
+
if (ctx) {
|
|
3025
|
+
ctx.nodeMap.clear();
|
|
3026
|
+
ctx.textureCache.clear();
|
|
3027
|
+
ctxRef.current = null;
|
|
3028
|
+
}
|
|
3029
|
+
initializedRef.current = false;
|
|
3030
|
+
initializingRef.current = false;
|
|
3031
|
+
};
|
|
3032
|
+
}, []);
|
|
3033
|
+
if (pixiAvailableRef.current === false) {
|
|
3034
|
+
return h3(
|
|
3035
|
+
"div",
|
|
3036
|
+
{
|
|
3037
|
+
className,
|
|
3038
|
+
style: {
|
|
3039
|
+
width,
|
|
3040
|
+
height,
|
|
3041
|
+
display: "flex",
|
|
3042
|
+
alignItems: "center",
|
|
3043
|
+
justifyContent: "center",
|
|
3044
|
+
backgroundColor: scene.background ?? "#1a1a2e",
|
|
3045
|
+
color: "#94a3b8",
|
|
3046
|
+
fontFamily: "monospace",
|
|
3047
|
+
fontSize: 14,
|
|
3048
|
+
textAlign: "center",
|
|
3049
|
+
padding: 20,
|
|
3050
|
+
boxSizing: "border-box",
|
|
3051
|
+
...containerStyle
|
|
3052
|
+
}
|
|
3053
|
+
},
|
|
3054
|
+
h3(
|
|
3055
|
+
"div",
|
|
3056
|
+
null,
|
|
3057
|
+
h3(
|
|
3058
|
+
"div",
|
|
3059
|
+
{ style: { marginBottom: 8, fontSize: 16, fontWeight: "bold", color: "#e2e8f0" } },
|
|
3060
|
+
"WebGL Renderer Unavailable"
|
|
3061
|
+
),
|
|
3062
|
+
h3(
|
|
3063
|
+
"div",
|
|
3064
|
+
null,
|
|
3065
|
+
"Install pixi.js to enable the WebGL renderer:"
|
|
3066
|
+
),
|
|
3067
|
+
h3(
|
|
3068
|
+
"code",
|
|
3069
|
+
{ style: { display: "block", marginTop: 8, padding: "8px 12px", backgroundColor: "#0f172a", borderRadius: 4 } },
|
|
3070
|
+
"npm install pixi.js"
|
|
3071
|
+
)
|
|
3072
|
+
)
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
return h3("div", {
|
|
3076
|
+
ref: containerRef,
|
|
3077
|
+
className,
|
|
3078
|
+
style: {
|
|
3079
|
+
width,
|
|
3080
|
+
height,
|
|
3081
|
+
overflow: "hidden",
|
|
3082
|
+
position: "relative",
|
|
3083
|
+
backgroundColor: scene.background ?? "#1a1a2e",
|
|
3084
|
+
...containerStyle
|
|
3085
|
+
}
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
// src/scene/renderer.ts
|
|
3090
|
+
var pixiAvailable = null;
|
|
3091
|
+
function isPixiAvailable() {
|
|
3092
|
+
if (pixiAvailable !== null) return pixiAvailable;
|
|
3093
|
+
try {
|
|
3094
|
+
const PIXI = globalThis.__PIXI ?? globalThis.PIXI;
|
|
3095
|
+
pixiAvailable = !!(PIXI && PIXI.Application);
|
|
3096
|
+
} catch {
|
|
3097
|
+
pixiAvailable = false;
|
|
3098
|
+
}
|
|
3099
|
+
return pixiAvailable;
|
|
3100
|
+
}
|
|
3101
|
+
function SceneRenderer(props) {
|
|
3102
|
+
if (isPixiAvailable()) {
|
|
3103
|
+
return PixiSceneRenderer(props);
|
|
3104
|
+
}
|
|
3105
|
+
return SvgSceneRenderer(props);
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// src/scene/schema.ts
|
|
3109
|
+
function createSceneSchemas(z) {
|
|
3110
|
+
const vec2 = z.object({
|
|
3111
|
+
x: z.number(),
|
|
3112
|
+
y: z.number()
|
|
3113
|
+
});
|
|
3114
|
+
const transform = z.object({
|
|
3115
|
+
x: z.number().optional(),
|
|
3116
|
+
y: z.number().optional(),
|
|
3117
|
+
rotation: z.number().optional().describe("Rotation in degrees"),
|
|
3118
|
+
scaleX: z.number().optional(),
|
|
3119
|
+
scaleY: z.number().optional(),
|
|
3120
|
+
originX: z.number().optional().describe("Transform origin X (0-1)"),
|
|
3121
|
+
originY: z.number().optional().describe("Transform origin Y (0-1)")
|
|
3122
|
+
}).optional();
|
|
3123
|
+
const style = z.object({
|
|
3124
|
+
fill: z.string().optional().describe("CSS color or gradient ref url(#id)"),
|
|
3125
|
+
stroke: z.string().optional().describe("CSS color"),
|
|
3126
|
+
strokeWidth: z.number().optional(),
|
|
3127
|
+
strokeDasharray: z.string().optional().describe('e.g. "5,3"'),
|
|
3128
|
+
strokeLinecap: z.enum(["butt", "round", "square"]).optional(),
|
|
3129
|
+
strokeLinejoin: z.enum(["miter", "round", "bevel"]).optional(),
|
|
3130
|
+
opacity: z.number().optional().describe("0-1"),
|
|
3131
|
+
fillOpacity: z.number().optional(),
|
|
3132
|
+
strokeOpacity: z.number().optional(),
|
|
3133
|
+
filter: z.string().optional(),
|
|
3134
|
+
cursor: z.string().optional(),
|
|
3135
|
+
pointerEvents: z.enum(["auto", "none"]).optional(),
|
|
3136
|
+
visible: z.boolean().optional()
|
|
3137
|
+
}).optional();
|
|
3138
|
+
const textStyle = z.object({
|
|
3139
|
+
fill: z.string().optional(),
|
|
3140
|
+
stroke: z.string().optional(),
|
|
3141
|
+
strokeWidth: z.number().optional(),
|
|
3142
|
+
opacity: z.number().optional(),
|
|
3143
|
+
fontSize: z.number().optional(),
|
|
3144
|
+
fontFamily: z.string().optional(),
|
|
3145
|
+
fontWeight: z.union([z.number(), z.string()]).optional(),
|
|
3146
|
+
textAnchor: z.enum(["start", "middle", "end"]).optional(),
|
|
3147
|
+
dominantBaseline: z.enum(["auto", "middle", "hanging", "text-top"]).optional(),
|
|
3148
|
+
letterSpacing: z.number().optional()
|
|
3149
|
+
}).optional();
|
|
3150
|
+
const gradientStop = z.object({
|
|
3151
|
+
offset: z.number().describe("0 to 1"),
|
|
3152
|
+
color: z.string()
|
|
3153
|
+
});
|
|
3154
|
+
const linearGradient = z.object({
|
|
3155
|
+
type: z.literal("linear"),
|
|
3156
|
+
id: z.string(),
|
|
3157
|
+
x1: z.number(),
|
|
3158
|
+
y1: z.number(),
|
|
3159
|
+
x2: z.number(),
|
|
3160
|
+
y2: z.number(),
|
|
3161
|
+
stops: z.array(gradientStop)
|
|
3162
|
+
});
|
|
3163
|
+
const radialGradient = z.object({
|
|
3164
|
+
type: z.literal("radial"),
|
|
3165
|
+
id: z.string(),
|
|
3166
|
+
cx: z.number(),
|
|
3167
|
+
cy: z.number(),
|
|
3168
|
+
r: z.number(),
|
|
3169
|
+
fx: z.number().optional(),
|
|
3170
|
+
fy: z.number().optional(),
|
|
3171
|
+
stops: z.array(gradientStop)
|
|
3172
|
+
});
|
|
3173
|
+
const gradient = z.union([linearGradient, radialGradient]);
|
|
3174
|
+
const particleEmitter = z.object({
|
|
3175
|
+
x: z.number(),
|
|
3176
|
+
y: z.number(),
|
|
3177
|
+
rate: z.number().describe("Particles per second"),
|
|
3178
|
+
lifetime: z.number().describe("Particle lifetime in ms"),
|
|
3179
|
+
speed: z.object({ min: z.number(), max: z.number() }),
|
|
3180
|
+
direction: z.object({ min: z.number(), max: z.number() }).describe("Angle range in degrees"),
|
|
3181
|
+
gravity: z.number().optional(),
|
|
3182
|
+
color: z.union([z.string(), z.array(z.string())]).optional(),
|
|
3183
|
+
size: z.object({ min: z.number(), max: z.number() }).optional(),
|
|
3184
|
+
fadeOut: z.boolean().optional(),
|
|
3185
|
+
shape: z.enum(["circle", "square"]).optional()
|
|
3186
|
+
});
|
|
3187
|
+
const spriteAnimation = z.object({
|
|
3188
|
+
frames: z.array(z.number()),
|
|
3189
|
+
fps: z.number(),
|
|
3190
|
+
loop: z.boolean().optional(),
|
|
3191
|
+
playing: z.boolean().optional()
|
|
3192
|
+
});
|
|
3193
|
+
const nodeSchema = z.object({
|
|
3194
|
+
type: z.enum([
|
|
3195
|
+
"rect",
|
|
3196
|
+
"circle",
|
|
3197
|
+
"ellipse",
|
|
3198
|
+
"line",
|
|
3199
|
+
"polyline",
|
|
3200
|
+
"polygon",
|
|
3201
|
+
"path",
|
|
3202
|
+
"text",
|
|
3203
|
+
"image",
|
|
3204
|
+
"group",
|
|
3205
|
+
"sprite",
|
|
3206
|
+
"tilemap",
|
|
3207
|
+
"particles"
|
|
3208
|
+
]),
|
|
3209
|
+
id: z.string().optional().describe("Auto-generated if not provided"),
|
|
3210
|
+
name: z.string().optional(),
|
|
3211
|
+
transform,
|
|
3212
|
+
style,
|
|
3213
|
+
interactive: z.boolean().optional(),
|
|
3214
|
+
data: z.record(z.any()).optional(),
|
|
3215
|
+
// rect
|
|
3216
|
+
width: z.number().optional(),
|
|
3217
|
+
height: z.number().optional(),
|
|
3218
|
+
rx: z.number().optional(),
|
|
3219
|
+
ry: z.number().optional(),
|
|
3220
|
+
// circle
|
|
3221
|
+
radius: z.number().optional(),
|
|
3222
|
+
// line
|
|
3223
|
+
x2: z.number().optional(),
|
|
3224
|
+
y2: z.number().optional(),
|
|
3225
|
+
// polyline, polygon
|
|
3226
|
+
points: z.array(vec2).optional(),
|
|
3227
|
+
// path
|
|
3228
|
+
d: z.string().optional(),
|
|
3229
|
+
// text
|
|
3230
|
+
text: z.string().optional(),
|
|
3231
|
+
// image
|
|
3232
|
+
href: z.string().optional(),
|
|
3233
|
+
preserveAspectRatio: z.string().optional(),
|
|
3234
|
+
// group
|
|
3235
|
+
children: z.array(z.any()).optional(),
|
|
3236
|
+
clipPath: z.string().optional(),
|
|
3237
|
+
// sprite
|
|
3238
|
+
frameWidth: z.number().optional(),
|
|
3239
|
+
frameHeight: z.number().optional(),
|
|
3240
|
+
frame: z.number().optional(),
|
|
3241
|
+
columns: z.number().optional(),
|
|
3242
|
+
animation: spriteAnimation.optional(),
|
|
3243
|
+
// tilemap
|
|
3244
|
+
tileWidth: z.number().optional(),
|
|
3245
|
+
tileHeight: z.number().optional(),
|
|
3246
|
+
// columns already defined above
|
|
3247
|
+
// particles
|
|
3248
|
+
emitters: z.array(particleEmitter).optional(),
|
|
3249
|
+
maxParticles: z.number().optional()
|
|
3250
|
+
}).passthrough();
|
|
3251
|
+
return {
|
|
3252
|
+
vec2,
|
|
3253
|
+
transform,
|
|
3254
|
+
style,
|
|
3255
|
+
textStyle,
|
|
3256
|
+
gradientStop,
|
|
3257
|
+
linearGradient,
|
|
3258
|
+
radialGradient,
|
|
3259
|
+
gradient,
|
|
3260
|
+
particleEmitter,
|
|
3261
|
+
spriteAnimation,
|
|
3262
|
+
nodeSchema
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// src/scene/tools.ts
|
|
3267
|
+
function uid2() {
|
|
3268
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
3269
|
+
}
|
|
3270
|
+
function getScene(ctx) {
|
|
3271
|
+
return ctx.state._scene ?? {
|
|
3272
|
+
_sceneVersion: 1,
|
|
3273
|
+
root: { id: "root", type: "group", children: [] },
|
|
3274
|
+
camera: { x: 400, y: 300, zoom: 1 },
|
|
3275
|
+
background: "#1a1a2e",
|
|
3276
|
+
gradients: [],
|
|
3277
|
+
filters: [],
|
|
3278
|
+
width: 800,
|
|
3279
|
+
height: 600
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
function cloneScene(scene) {
|
|
3283
|
+
return JSON.parse(JSON.stringify(scene));
|
|
3284
|
+
}
|
|
3285
|
+
function findNode(node, id) {
|
|
3286
|
+
if (node.id === id) return node;
|
|
3287
|
+
if (node.type === "group" && node.children) {
|
|
3288
|
+
for (const child of node.children) {
|
|
3289
|
+
const found = findNode(child, id);
|
|
3290
|
+
if (found) return found;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
return null;
|
|
3294
|
+
}
|
|
3295
|
+
function removeNode(root, id) {
|
|
3296
|
+
if (!root.children) return false;
|
|
3297
|
+
const idx = root.children.findIndex((c) => c.id === id);
|
|
3298
|
+
if (idx !== -1) {
|
|
3299
|
+
root.children.splice(idx, 1);
|
|
3300
|
+
return true;
|
|
3301
|
+
}
|
|
3302
|
+
for (const child of root.children) {
|
|
3303
|
+
if (child.type === "group" && removeNode(child, id)) return true;
|
|
3304
|
+
}
|
|
3305
|
+
return false;
|
|
3306
|
+
}
|
|
3307
|
+
function findGroup(root, id) {
|
|
3308
|
+
const node = findNode(root, id);
|
|
3309
|
+
if (node && node.type === "group") return node;
|
|
3310
|
+
return null;
|
|
3311
|
+
}
|
|
3312
|
+
function createSceneTools(namespace, z) {
|
|
3313
|
+
const schemas = createSceneSchemas(z);
|
|
3314
|
+
return [
|
|
3315
|
+
// ── scene.add ──────────────────────────────────────────────────────
|
|
3316
|
+
{
|
|
3317
|
+
name: `${namespace}.add`,
|
|
3318
|
+
description: `Add a visual node to the scene. Returns the node's ID.
|
|
3319
|
+
|
|
3320
|
+
Node types: rect, circle, ellipse, line, polyline, polygon, path, text, image, group, sprite, tilemap, particles.
|
|
3321
|
+
|
|
3322
|
+
Each node can have: transform (position/rotation/scale), style (fill/stroke/opacity), name, interactive, data (metadata).
|
|
3323
|
+
|
|
3324
|
+
For visually rich scenes: prefer "path" nodes with cubic bezier curves (C commands) over basic shapes for organic forms, compose entities as "group" nodes with layered children, and use gradients (defined via scene.set) for depth.
|
|
3325
|
+
|
|
3326
|
+
Examples:
|
|
3327
|
+
- Curved path: { node: { type: "path", d: "M 0 0 C 8 -18 30 -22 50 -12 C 60 -6 60 6 50 12 C 30 22 8 18 0 0 Z", style: { fill: "url(#my-gradient)", stroke: "#0e7490", strokeWidth: 0.8 } } }
|
|
3328
|
+
- Composed entity: { node: { type: "group", name: "creature", data: { entityType: "fish" }, transform: { x: 300, y: 200 }, children: [{ type: "path", d: "M 0 0 C 8 -18 30 -22 50 -12 C 60 -6 60 6 50 12 C 30 22 8 18 0 0 Z", style: { fill: "url(#body-grad)" } }, { type: "circle", radius: 3, transform: { x: 40, y: -3 }, style: { fill: "#0f172a" } }] } }
|
|
3329
|
+
- Rectangle: { node: { type: "rect", width: 100, height: 50, transform: { x: 200, y: 100 }, style: { fill: "#ef4444" } } }
|
|
3330
|
+
- Text: { node: { type: "text", text: "Hello", transform: { x: 100, y: 50 }, style: { fill: "#fff", fontSize: 24, textAnchor: "middle" } } }
|
|
3331
|
+
- Image: { node: { type: "image", href: "https://example.com/img.png", width: 200, height: 150 } }`,
|
|
3332
|
+
input_schema: z.object({
|
|
3333
|
+
node: schemas.nodeSchema.describe("Scene node to add"),
|
|
3334
|
+
parentId: z.string().optional().describe("Parent group ID. Defaults to root.")
|
|
3335
|
+
}),
|
|
3336
|
+
risk: "low",
|
|
3337
|
+
capabilities_required: ["state.write"],
|
|
3338
|
+
handler: async (ctx, input) => {
|
|
3339
|
+
const scene = cloneScene(getScene(ctx));
|
|
3340
|
+
const nodeId = input.node.id ?? uid2();
|
|
3341
|
+
const node = { ...input.node, id: nodeId };
|
|
3342
|
+
if (node.type === "group" && !node.children) node.children = [];
|
|
3343
|
+
const parent = input.parentId ? findGroup(scene.root, input.parentId) : scene.root;
|
|
3344
|
+
if (!parent) throw new Error(`Parent group "${input.parentId}" not found`);
|
|
3345
|
+
parent.children.push(node);
|
|
3346
|
+
ctx.setState({ ...ctx.state, _scene: scene });
|
|
3347
|
+
return { nodeId };
|
|
3348
|
+
}
|
|
3349
|
+
},
|
|
3350
|
+
// ── scene.update ───────────────────────────────────────────────────
|
|
3351
|
+
{
|
|
3352
|
+
name: `${namespace}.update`,
|
|
3353
|
+
description: `Update an existing scene node. Merges transform, style, and any other properties.
|
|
3354
|
+
|
|
3355
|
+
Use this for ALL node modifications: moving, resizing, recoloring, animating, renaming, etc.
|
|
3356
|
+
|
|
3357
|
+
To animate: include a "tween" object with { property, from, to, duration, easing?, repeat?, yoyo? }.
|
|
3358
|
+
Easing options: linear, ease-in, ease-out, ease-in-out, ease-in-quad, ease-out-quad, ease-in-cubic, ease-out-cubic, ease-in-elastic, ease-out-elastic, ease-in-bounce, ease-out-bounce.
|
|
3359
|
+
|
|
3360
|
+
Examples:
|
|
3361
|
+
- Move: { nodeId: "abc", transform: { x: 200, y: 100 } }
|
|
3362
|
+
- Restyle: { nodeId: "abc", style: { fill: "#22c55e", opacity: 0.8 } }
|
|
3363
|
+
- Resize: { nodeId: "abc", width: 200, height: 100 }
|
|
3364
|
+
- Change text: { nodeId: "abc", text: "New text" }
|
|
3365
|
+
- Animate slide: { nodeId: "abc", tween: { property: "transform.x", from: 0, to: 300, duration: 1000, easing: "ease-out" } }
|
|
3366
|
+
- Animate pulse: { nodeId: "abc", tween: { property: "transform.scaleX", from: 1, to: 1.2, duration: 400, repeat: -1, yoyo: true } }
|
|
3367
|
+
- Animate fade: { nodeId: "abc", tween: { property: "style.opacity", from: 0, to: 1, duration: 500 } }`,
|
|
3368
|
+
input_schema: z.object({
|
|
3369
|
+
nodeId: z.string().describe("ID of the node to update"),
|
|
3370
|
+
transform: schemas.transform.describe("Position, rotation, scale"),
|
|
3371
|
+
style: schemas.style.describe("Fill, stroke, opacity, etc."),
|
|
3372
|
+
tween: z.object({
|
|
3373
|
+
property: z.string().describe("Dot-path: transform.x, style.opacity, etc."),
|
|
3374
|
+
from: z.number(),
|
|
3375
|
+
to: z.number(),
|
|
3376
|
+
duration: z.number().describe("Milliseconds"),
|
|
3377
|
+
easing: z.string().optional(),
|
|
3378
|
+
delay: z.number().optional(),
|
|
3379
|
+
repeat: z.number().optional().describe("-1 = infinite"),
|
|
3380
|
+
yoyo: z.boolean().optional()
|
|
3381
|
+
}).optional().describe("Tween animation"),
|
|
3382
|
+
// Type-specific props (all optional)
|
|
3383
|
+
width: z.number().optional(),
|
|
3384
|
+
height: z.number().optional(),
|
|
3385
|
+
radius: z.number().optional(),
|
|
3386
|
+
rx: z.number().optional(),
|
|
3387
|
+
ry: z.number().optional(),
|
|
3388
|
+
text: z.string().optional(),
|
|
3389
|
+
href: z.string().optional(),
|
|
3390
|
+
d: z.string().optional(),
|
|
3391
|
+
points: z.array(schemas.vec2).optional(),
|
|
3392
|
+
x2: z.number().optional(),
|
|
3393
|
+
y2: z.number().optional(),
|
|
3394
|
+
name: z.string().optional(),
|
|
3395
|
+
interactive: z.boolean().optional(),
|
|
3396
|
+
data: z.record(z.any()).optional(),
|
|
3397
|
+
frame: z.number().optional()
|
|
3398
|
+
}).passthrough(),
|
|
3399
|
+
risk: "low",
|
|
3400
|
+
capabilities_required: ["state.write"],
|
|
3401
|
+
handler: async (ctx, input) => {
|
|
3402
|
+
const scene = cloneScene(getScene(ctx));
|
|
3403
|
+
const node = findNode(scene.root, input.nodeId);
|
|
3404
|
+
if (!node) throw new Error(`Node "${input.nodeId}" not found`);
|
|
3405
|
+
const { nodeId: _, ...props } = input;
|
|
3406
|
+
if (props.transform) {
|
|
3407
|
+
node.transform = { ...node.transform ?? {}, ...props.transform };
|
|
3408
|
+
delete props.transform;
|
|
3409
|
+
}
|
|
3410
|
+
if (props.style) {
|
|
3411
|
+
node.style = { ...node.style ?? {}, ...props.style };
|
|
3412
|
+
delete props.style;
|
|
3413
|
+
}
|
|
3414
|
+
if (props.tween) {
|
|
3415
|
+
node.tween = { ...props.tween, startedAt: ctx.timestamp };
|
|
3416
|
+
delete props.tween;
|
|
3417
|
+
}
|
|
3418
|
+
const { id: _id, type: _type, ...safeProps } = props;
|
|
3419
|
+
Object.assign(node, safeProps);
|
|
3420
|
+
ctx.setState({ ...ctx.state, _scene: scene });
|
|
3421
|
+
return { updated: true };
|
|
3422
|
+
}
|
|
3423
|
+
},
|
|
3424
|
+
// ── scene.remove ───────────────────────────────────────────────────
|
|
3425
|
+
{
|
|
3426
|
+
name: `${namespace}.remove`,
|
|
3427
|
+
description: `Remove one or more nodes from the scene by ID. Removes all children if a node is a group.
|
|
3428
|
+
|
|
3429
|
+
Examples:
|
|
3430
|
+
- Single: { nodeIds: ["abc123"] }
|
|
3431
|
+
- Multiple: { nodeIds: ["node1", "node2", "node3"] }
|
|
3432
|
+
- Clear all: { clear: true }`,
|
|
3433
|
+
input_schema: z.object({
|
|
3434
|
+
nodeIds: z.array(z.string()).optional().describe("IDs of nodes to remove"),
|
|
3435
|
+
clear: z.boolean().optional().describe("If true, remove ALL nodes")
|
|
3436
|
+
}),
|
|
3437
|
+
risk: "low",
|
|
3438
|
+
capabilities_required: ["state.write"],
|
|
3439
|
+
handler: async (ctx, input) => {
|
|
3440
|
+
const scene = cloneScene(getScene(ctx));
|
|
3441
|
+
if (input.clear) {
|
|
3442
|
+
scene.root.children = [];
|
|
3443
|
+
ctx.setState({ ...ctx.state, _scene: scene });
|
|
3444
|
+
return { removed: "all" };
|
|
3445
|
+
}
|
|
3446
|
+
const removed = [];
|
|
3447
|
+
for (const id of input.nodeIds ?? []) {
|
|
3448
|
+
if (removeNode(scene.root, id)) removed.push(id);
|
|
3449
|
+
}
|
|
3450
|
+
ctx.setState({ ...ctx.state, _scene: scene });
|
|
3451
|
+
return { removed };
|
|
3452
|
+
}
|
|
3453
|
+
},
|
|
3454
|
+
// ── scene.set ──────────────────────────────────────────────────────
|
|
3455
|
+
{
|
|
3456
|
+
name: `${namespace}.set`,
|
|
3457
|
+
description: `Set scene-level properties: camera position/zoom, background color, gradients, dimensions.
|
|
3458
|
+
|
|
3459
|
+
Define gradients early \u2014 natural scenes should have 3-5 gradients (water, sky, foliage, light sources). Reference them in node styles with fill: "url(#gradientId)". Gradients add depth and richness that flat colors cannot.
|
|
3460
|
+
|
|
3461
|
+
Examples:
|
|
3462
|
+
- Background: { background: "#0f172a" }
|
|
3463
|
+
- Camera pan: { camera: { x: 500, y: 300 } }
|
|
3464
|
+
- Camera zoom: { camera: { zoom: 2 } }
|
|
3465
|
+
- Linear gradient: { gradient: { type: "linear", id: "sunset", x1: 0, y1: 0, x2: 1, y2: 1, stops: [{ offset: 0, color: "#f97316" }, { offset: 0.5, color: "#ec4899" }, { offset: 1, color: "#8b5cf6" }] } }
|
|
3466
|
+
- Radial gradient: { gradient: { type: "radial", id: "glow", cx: 0.5, cy: 0.5, r: 0.5, stops: [{ offset: 0, color: "#fef3c7" }, { offset: 1, color: "transparent" }] } }
|
|
3467
|
+
Then use in style: { fill: "url(#sunset)" } or { fill: "url(#glow)" }
|
|
3468
|
+
- Resize: { width: 1024, height: 768 }`,
|
|
3469
|
+
input_schema: z.object({
|
|
3470
|
+
background: z.string().optional().describe("CSS background color"),
|
|
3471
|
+
camera: z.object({
|
|
3472
|
+
x: z.number().optional(),
|
|
3473
|
+
y: z.number().optional(),
|
|
3474
|
+
zoom: z.number().optional().describe("1 = 100%, 2 = zoomed in, 0.5 = zoomed out"),
|
|
3475
|
+
rotation: z.number().optional()
|
|
3476
|
+
}).optional(),
|
|
3477
|
+
gradient: schemas.gradient.optional().describe("Add/update a reusable gradient definition"),
|
|
3478
|
+
width: z.number().optional(),
|
|
3479
|
+
height: z.number().optional()
|
|
3480
|
+
}),
|
|
3481
|
+
risk: "low",
|
|
3482
|
+
capabilities_required: ["state.write"],
|
|
3483
|
+
handler: async (ctx, input) => {
|
|
3484
|
+
const scene = cloneScene(getScene(ctx));
|
|
3485
|
+
if (input.background != null) scene.background = input.background;
|
|
3486
|
+
if (input.width != null) scene.width = input.width;
|
|
3487
|
+
if (input.height != null) scene.height = input.height;
|
|
3488
|
+
if (input.camera) {
|
|
3489
|
+
scene.camera = { ...scene.camera ?? { x: 400, y: 300, zoom: 1 }, ...input.camera };
|
|
3490
|
+
}
|
|
3491
|
+
if (input.gradient) {
|
|
3492
|
+
if (!scene.gradients) scene.gradients = [];
|
|
3493
|
+
const idx = scene.gradients.findIndex((g) => g.id === input.gradient.id);
|
|
3494
|
+
if (idx !== -1) {
|
|
3495
|
+
scene.gradients[idx] = input.gradient;
|
|
3496
|
+
} else {
|
|
3497
|
+
scene.gradients.push(input.gradient);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
ctx.setState({ ...ctx.state, _scene: scene });
|
|
3501
|
+
return { updated: true };
|
|
3502
|
+
}
|
|
3503
|
+
},
|
|
3504
|
+
// ── scene.batch ────────────────────────────────────────────────────
|
|
3505
|
+
{
|
|
3506
|
+
name: `${namespace}.batch`,
|
|
3507
|
+
description: `Execute multiple scene operations in a single state update. More efficient than individual tool calls for building complex scenes.
|
|
3508
|
+
|
|
3509
|
+
Each operation has an "op" field: "add", "update", "remove", "set".
|
|
3510
|
+
|
|
3511
|
+
Best practice: define gradients first (op: "set"), then add entities that reference them. Compose organic entities as groups with path children using cubic bezier curves.
|
|
3512
|
+
|
|
3513
|
+
Example \u2014 build a rich scene in one call:
|
|
3514
|
+
{ operations: [
|
|
3515
|
+
{ op: "set", background: "#0f172a" },
|
|
3516
|
+
{ op: "set", gradient: { type: "linear", id: "creature-grad", x1: 0, y1: 0, x2: 0, y2: 1, stops: [{ offset: 0, color: "#a78bfa" }, { offset: 1, color: "#4c1d95" }] } },
|
|
3517
|
+
{ op: "add", node: { type: "group", id: "creature", transform: { x: 400, y: 300 }, data: { entityType: "creature" }, children: [
|
|
3518
|
+
{ type: "path", d: "M 0 0 C 10 -20 35 -25 55 -10 C 65 0 65 10 55 18 C 35 28 10 20 0 0 Z", style: { fill: "url(#creature-grad)", stroke: "#7c3aed", strokeWidth: 0.8 } },
|
|
3519
|
+
{ type: "circle", radius: 3, transform: { x: 42, y: -2 }, style: { fill: "#1e1b4b" } },
|
|
3520
|
+
{ type: "path", d: "M 15 -8 C 25 -16 40 -15 50 -6", style: { fill: "none", stroke: "rgba(255,255,255,0.2)", strokeWidth: 2 } }
|
|
3521
|
+
] } },
|
|
3522
|
+
{ op: "add", node: { type: "text", text: "Welcome", transform: { x: 400, y: 80 }, style: { fill: "#fff", fontSize: 32, textAnchor: "middle" } } },
|
|
3523
|
+
{ op: "update", nodeId: "creature", tween: { property: "transform.y", from: 300, to: 290, duration: 2000, repeat: -1, yoyo: true, easing: "ease-in-out" } }
|
|
3524
|
+
] }`,
|
|
3525
|
+
input_schema: z.object({
|
|
3526
|
+
operations: z.array(z.record(z.any())).describe('Operations with "op" field: add, update, remove, set')
|
|
3527
|
+
}),
|
|
3528
|
+
risk: "low",
|
|
3529
|
+
capabilities_required: ["state.write"],
|
|
3530
|
+
handler: async (ctx, input) => {
|
|
3531
|
+
const scene = cloneScene(getScene(ctx));
|
|
3532
|
+
const results = [];
|
|
3533
|
+
for (const op of input.operations) {
|
|
3534
|
+
try {
|
|
3535
|
+
switch (op.op) {
|
|
3536
|
+
case "add": {
|
|
3537
|
+
const nodeId = op.node?.id ?? op.id ?? uid2();
|
|
3538
|
+
const node = { ...op.node, id: nodeId };
|
|
3539
|
+
if (node.type === "group" && !node.children) node.children = [];
|
|
3540
|
+
const parent = op.parentId ? findGroup(scene.root, op.parentId) : scene.root;
|
|
3541
|
+
if (!parent) throw new Error(`Parent "${op.parentId}" not found`);
|
|
3542
|
+
parent.children.push(node);
|
|
3543
|
+
results.push({ op: "add", nodeId });
|
|
3544
|
+
break;
|
|
3545
|
+
}
|
|
3546
|
+
case "update": {
|
|
3547
|
+
const node = findNode(scene.root, op.nodeId);
|
|
3548
|
+
if (!node) throw new Error(`Node "${op.nodeId}" not found`);
|
|
3549
|
+
const { nodeId: _n, op: _o, ...props } = op;
|
|
3550
|
+
if (props.transform) {
|
|
3551
|
+
node.transform = { ...node.transform ?? {}, ...props.transform };
|
|
3552
|
+
delete props.transform;
|
|
3553
|
+
}
|
|
3554
|
+
if (props.style) {
|
|
3555
|
+
node.style = { ...node.style ?? {}, ...props.style };
|
|
3556
|
+
delete props.style;
|
|
3557
|
+
}
|
|
3558
|
+
if (props.tween) {
|
|
3559
|
+
node.tween = { ...props.tween, startedAt: ctx.timestamp };
|
|
3560
|
+
delete props.tween;
|
|
3561
|
+
}
|
|
3562
|
+
const { id: _id, type: _type, ...safeProps } = props;
|
|
3563
|
+
Object.assign(node, safeProps);
|
|
3564
|
+
results.push({ op: "update", nodeId: op.nodeId });
|
|
3565
|
+
break;
|
|
3566
|
+
}
|
|
3567
|
+
case "remove": {
|
|
3568
|
+
if (op.clear) {
|
|
3569
|
+
scene.root.children = [];
|
|
3570
|
+
results.push({ op: "remove", cleared: true });
|
|
3571
|
+
} else if (op.nodeIds) {
|
|
3572
|
+
for (const id of op.nodeIds) removeNode(scene.root, id);
|
|
3573
|
+
results.push({ op: "remove", nodeIds: op.nodeIds });
|
|
3574
|
+
} else if (op.nodeId) {
|
|
3575
|
+
removeNode(scene.root, op.nodeId);
|
|
3576
|
+
results.push({ op: "remove", nodeId: op.nodeId });
|
|
3577
|
+
}
|
|
3578
|
+
break;
|
|
3579
|
+
}
|
|
3580
|
+
case "set": {
|
|
3581
|
+
const { op: _o, ...setProps } = op;
|
|
3582
|
+
if (setProps.background != null) scene.background = setProps.background;
|
|
3583
|
+
if (setProps.width != null) scene.width = setProps.width;
|
|
3584
|
+
if (setProps.height != null) scene.height = setProps.height;
|
|
3585
|
+
if (setProps.camera) {
|
|
3586
|
+
scene.camera = { ...scene.camera ?? { x: 400, y: 300, zoom: 1 }, ...setProps.camera };
|
|
3587
|
+
}
|
|
3588
|
+
if (setProps.gradient) {
|
|
3589
|
+
if (!scene.gradients) scene.gradients = [];
|
|
3590
|
+
const idx = scene.gradients.findIndex((g) => g.id === setProps.gradient.id);
|
|
3591
|
+
if (idx !== -1) scene.gradients[idx] = setProps.gradient;
|
|
3592
|
+
else scene.gradients.push(setProps.gradient);
|
|
3593
|
+
}
|
|
3594
|
+
results.push({ op: "set" });
|
|
3595
|
+
break;
|
|
3596
|
+
}
|
|
3597
|
+
default:
|
|
3598
|
+
results.push({ op: op.op, error: `Unknown operation: ${op.op}` });
|
|
3599
|
+
}
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
results.push({ op: op.op, error: err.message });
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
ctx.setState({ ...ctx.state, _scene: scene });
|
|
3605
|
+
return { applied: results.length, results };
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
];
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/scene/particles.ts
|
|
3612
|
+
function rand(min, max) {
|
|
3613
|
+
return min + Math.random() * (max - min);
|
|
3614
|
+
}
|
|
3615
|
+
function degToRad(deg) {
|
|
3616
|
+
return deg * Math.PI / 180;
|
|
3617
|
+
}
|
|
3618
|
+
function spawnParticles(emitter, dt) {
|
|
3619
|
+
const count = Math.floor(emitter.rate * (dt / 1e3));
|
|
3620
|
+
const particles = [];
|
|
3621
|
+
for (let i = 0; i < count; i++) {
|
|
3622
|
+
const angle = degToRad(rand(emitter.direction.min, emitter.direction.max));
|
|
3623
|
+
const speed = rand(emitter.speed.min, emitter.speed.max);
|
|
3624
|
+
const size = emitter.size ? rand(emitter.size.min, emitter.size.max) : 4;
|
|
3625
|
+
const colors = Array.isArray(emitter.color) ? emitter.color : [emitter.color ?? "#ffffff"];
|
|
3626
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
3627
|
+
particles.push({
|
|
3628
|
+
x: emitter.x,
|
|
3629
|
+
y: emitter.y,
|
|
3630
|
+
vx: Math.cos(angle) * speed,
|
|
3631
|
+
vy: Math.sin(angle) * speed,
|
|
3632
|
+
age: 0,
|
|
3633
|
+
lifetime: emitter.lifetime,
|
|
3634
|
+
size,
|
|
3635
|
+
color
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
return particles;
|
|
3639
|
+
}
|
|
3640
|
+
function tickParticles(particles, emitters, dt) {
|
|
3641
|
+
const dtSec = dt / 1e3;
|
|
3642
|
+
const gravity = emitters[0]?.gravity ?? 0;
|
|
3643
|
+
return particles.map((p) => ({
|
|
3644
|
+
...p,
|
|
3645
|
+
x: p.x + p.vx * dtSec,
|
|
3646
|
+
y: p.y + p.vy * dtSec,
|
|
3647
|
+
vx: p.vx,
|
|
3648
|
+
vy: p.vy + gravity * dtSec,
|
|
3649
|
+
age: p.age + dt
|
|
3650
|
+
})).filter((p) => p.age < p.lifetime);
|
|
3651
|
+
}
|
|
3652
|
+
function tickParticleNode(node, dt) {
|
|
3653
|
+
const max = node.maxParticles ?? 200;
|
|
3654
|
+
let particles = node._particles ? [...node._particles] : [];
|
|
3655
|
+
particles = tickParticles(particles, node.emitters, dt);
|
|
3656
|
+
for (const emitter of node.emitters) {
|
|
3657
|
+
const spawned = spawnParticles(emitter, dt);
|
|
3658
|
+
particles.push(...spawned);
|
|
3659
|
+
}
|
|
3660
|
+
if (particles.length > max) {
|
|
3661
|
+
particles = particles.slice(particles.length - max);
|
|
3662
|
+
}
|
|
3663
|
+
return particles;
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// src/scene/hooks.ts
|
|
3667
|
+
function getReact5() {
|
|
3668
|
+
const R = globalThis.React;
|
|
3669
|
+
if (!R) throw new Error("React is not available. Hooks must be used inside a Canvas component.");
|
|
3670
|
+
return R;
|
|
3671
|
+
}
|
|
3672
|
+
var React2 = new Proxy({}, {
|
|
3673
|
+
get(_target, prop) {
|
|
3674
|
+
return getReact5()[prop];
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
function useSceneInteraction() {
|
|
3678
|
+
const [lastEvent, setLastEvent] = React2.useState(null);
|
|
3679
|
+
const [hoveredNodeId, setHoveredNodeId] = React2.useState(null);
|
|
3680
|
+
const onNodeClick = React2.useCallback((nodeId, event) => {
|
|
3681
|
+
setLastEvent({ type: "click", nodeId, x: event.x, y: event.y });
|
|
3682
|
+
}, []);
|
|
3683
|
+
const onNodeHover = React2.useCallback((nodeId) => {
|
|
3684
|
+
setHoveredNodeId(nodeId);
|
|
3685
|
+
if (nodeId) {
|
|
3686
|
+
setLastEvent({ type: "hover", nodeId, x: 0, y: 0 });
|
|
3687
|
+
}
|
|
3688
|
+
}, []);
|
|
3689
|
+
return { lastEvent, hoveredNodeId, onNodeClick, onNodeHover };
|
|
3690
|
+
}
|
|
3691
|
+
function useSceneDrag(callTool, toolNamespace = "scene") {
|
|
3692
|
+
const [dragging, setDragging] = React2.useState(null);
|
|
3693
|
+
const [dragOffset, setDragOffset] = React2.useState(null);
|
|
3694
|
+
const startRef = React2.useRef(null);
|
|
3695
|
+
const onNodeDragStart = React2.useCallback((nodeId, pos) => {
|
|
3696
|
+
setDragging(nodeId);
|
|
3697
|
+
startRef.current = pos;
|
|
3698
|
+
setDragOffset({ x: 0, y: 0 });
|
|
3699
|
+
}, []);
|
|
3700
|
+
const onNodeDrag = React2.useCallback((nodeId, pos) => {
|
|
3701
|
+
if (!startRef.current) return;
|
|
3702
|
+
setDragOffset({
|
|
3703
|
+
x: pos.x - startRef.current.x,
|
|
3704
|
+
y: pos.y - startRef.current.y
|
|
3705
|
+
});
|
|
3706
|
+
}, []);
|
|
3707
|
+
const onNodeDragEnd = React2.useCallback((nodeId, pos) => {
|
|
3708
|
+
if (!startRef.current) return;
|
|
3709
|
+
const dx = pos.x - startRef.current.x;
|
|
3710
|
+
const dy = pos.y - startRef.current.y;
|
|
3711
|
+
setDragging(null);
|
|
3712
|
+
setDragOffset(null);
|
|
3713
|
+
startRef.current = null;
|
|
3714
|
+
callTool(`${toolNamespace}.update`, {
|
|
3715
|
+
nodeId,
|
|
3716
|
+
transform: { x: dx, y: dy }
|
|
3717
|
+
}).catch(() => {
|
|
3718
|
+
});
|
|
3719
|
+
}, [callTool, toolNamespace]);
|
|
3720
|
+
return { dragging, dragOffset, onNodeDragStart, onNodeDrag, onNodeDragEnd };
|
|
3721
|
+
}
|
|
3722
|
+
function useSceneSelection() {
|
|
3723
|
+
const [selectedIds, setSelectedIds] = React2.useState([]);
|
|
3724
|
+
const select = React2.useCallback((nodeId) => {
|
|
3725
|
+
setSelectedIds((prev) => prev.includes(nodeId) ? prev : [...prev, nodeId]);
|
|
3726
|
+
}, []);
|
|
3727
|
+
const deselect = React2.useCallback((nodeId) => {
|
|
3728
|
+
setSelectedIds((prev) => prev.filter((id) => id !== nodeId));
|
|
3729
|
+
}, []);
|
|
3730
|
+
const toggle = React2.useCallback((nodeId) => {
|
|
3731
|
+
setSelectedIds(
|
|
3732
|
+
(prev) => prev.includes(nodeId) ? prev.filter((id) => id !== nodeId) : [...prev, nodeId]
|
|
3733
|
+
);
|
|
3734
|
+
}, []);
|
|
3735
|
+
const clear = React2.useCallback(() => setSelectedIds([]), []);
|
|
3736
|
+
const isSelected = React2.useCallback(
|
|
3737
|
+
(nodeId) => selectedIds.includes(nodeId),
|
|
3738
|
+
[selectedIds]
|
|
3739
|
+
);
|
|
3740
|
+
return { selectedIds, select, deselect, toggle, clear, isSelected };
|
|
3741
|
+
}
|
|
3742
|
+
function useSceneViewport(callTool, scene, toolNamespace = "scene") {
|
|
3743
|
+
const camera = scene.camera ?? { x: 400, y: 300, zoom: 1 };
|
|
3744
|
+
const timerRef = React2.useRef(null);
|
|
3745
|
+
const commitCamera = React2.useCallback((cam) => {
|
|
3746
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3747
|
+
timerRef.current = setTimeout(() => {
|
|
3748
|
+
callTool(`${toolNamespace}.set`, { camera: cam }).catch(() => {
|
|
3749
|
+
});
|
|
3750
|
+
}, 200);
|
|
3751
|
+
}, [callTool, toolNamespace]);
|
|
3752
|
+
const onViewportPan = React2.useCallback((delta) => {
|
|
3753
|
+
commitCamera({ x: camera.x + delta.x, y: camera.y + delta.y });
|
|
3754
|
+
}, [camera, commitCamera]);
|
|
3755
|
+
const onViewportZoom = React2.useCallback((newZoom, _center) => {
|
|
3756
|
+
commitCamera({ zoom: Math.max(0.1, Math.min(10, newZoom)) });
|
|
3757
|
+
}, [commitCamera]);
|
|
3758
|
+
React2.useEffect(() => {
|
|
3759
|
+
return () => {
|
|
3760
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3761
|
+
};
|
|
3762
|
+
}, []);
|
|
3763
|
+
return { camera, onViewportPan, onViewportZoom };
|
|
3764
|
+
}
|
|
3765
|
+
function useSceneTweens(scene) {
|
|
3766
|
+
const [, forceRender] = React2.useState(0);
|
|
3767
|
+
const rafRef = React2.useRef(null);
|
|
3768
|
+
const hasTweens = React2.useRef(false);
|
|
3769
|
+
hasTweens.current = false;
|
|
3770
|
+
const checkTweens = (node) => {
|
|
3771
|
+
if (node.tween?.startedAt != null) hasTweens.current = true;
|
|
3772
|
+
if (node.type === "group" && node.children) {
|
|
3773
|
+
for (const child of node.children) checkTweens(child);
|
|
3774
|
+
}
|
|
3775
|
+
};
|
|
3776
|
+
checkTweens(scene.root);
|
|
3777
|
+
React2.useEffect(() => {
|
|
3778
|
+
if (!hasTweens.current) return;
|
|
3779
|
+
const tick = () => {
|
|
3780
|
+
forceRender((n) => n + 1);
|
|
3781
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
3782
|
+
};
|
|
3783
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
3784
|
+
return () => {
|
|
3785
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
3786
|
+
};
|
|
3787
|
+
}, [scene]);
|
|
3788
|
+
if (!hasTweens.current) return scene;
|
|
3789
|
+
const now = Date.now();
|
|
3790
|
+
return applyTweens2(scene, now);
|
|
3791
|
+
}
|
|
3792
|
+
function applyTweens2(scene, now) {
|
|
3793
|
+
const newRoot = applyTweensToNode(scene.root, now);
|
|
3794
|
+
if (newRoot === scene.root) return scene;
|
|
3795
|
+
return { ...scene, root: newRoot };
|
|
3796
|
+
}
|
|
3797
|
+
function applyTweensToNode(node, now) {
|
|
3798
|
+
let modified = false;
|
|
3799
|
+
let result = node;
|
|
3800
|
+
if (node.tween?.startedAt != null) {
|
|
3801
|
+
const value = interpolateTween(node.tween, now);
|
|
3802
|
+
if (value != null) {
|
|
3803
|
+
result = setPath({ ...node }, node.tween.property, value);
|
|
3804
|
+
modified = true;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
if (node.type === "group" && node.children) {
|
|
3808
|
+
const children = node.children;
|
|
3809
|
+
const newChildren = children.map((child) => applyTweensToNode(child, now));
|
|
3810
|
+
const childrenChanged = newChildren.some((c, i) => c !== children[i]);
|
|
3811
|
+
if (childrenChanged) {
|
|
3812
|
+
result = { ...result, children: newChildren };
|
|
3813
|
+
modified = true;
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
return modified ? result : node;
|
|
3817
|
+
}
|
|
3818
|
+
function useParticleTick(scene) {
|
|
3819
|
+
const [, forceRender] = React2.useState(0);
|
|
3820
|
+
const rafRef = React2.useRef(null);
|
|
3821
|
+
const lastTimeRef = React2.useRef(Date.now());
|
|
3822
|
+
const particleStateRef = React2.useRef(/* @__PURE__ */ new Map());
|
|
3823
|
+
const hasParticles = React2.useRef(false);
|
|
3824
|
+
hasParticles.current = false;
|
|
3825
|
+
const checkParticles = (node) => {
|
|
3826
|
+
if (node.type === "particles") hasParticles.current = true;
|
|
3827
|
+
if (node.type === "group" && node.children) {
|
|
3828
|
+
for (const child of node.children) checkParticles(child);
|
|
3829
|
+
}
|
|
3830
|
+
};
|
|
3831
|
+
checkParticles(scene.root);
|
|
3832
|
+
React2.useEffect(() => {
|
|
3833
|
+
if (!hasParticles.current) return;
|
|
3834
|
+
const tick = () => {
|
|
3835
|
+
const now = Date.now();
|
|
3836
|
+
const dt = Math.min(now - lastTimeRef.current, 100);
|
|
3837
|
+
lastTimeRef.current = now;
|
|
3838
|
+
const updateNode = (node) => {
|
|
3839
|
+
if (node.type === "particles") {
|
|
3840
|
+
const current = particleStateRef.current.get(node.id) ?? [];
|
|
3841
|
+
const fakeNode = { ...node, _particles: current };
|
|
3842
|
+
const updated = tickParticleNode(fakeNode, dt);
|
|
3843
|
+
particleStateRef.current.set(node.id, updated);
|
|
3844
|
+
}
|
|
3845
|
+
if (node.type === "group" && node.children) {
|
|
3846
|
+
for (const child of node.children) updateNode(child);
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
updateNode(scene.root);
|
|
3850
|
+
forceRender((n) => n + 1);
|
|
3851
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
3852
|
+
};
|
|
3853
|
+
lastTimeRef.current = Date.now();
|
|
3854
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
3855
|
+
return () => {
|
|
3856
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
3857
|
+
};
|
|
3858
|
+
}, [scene]);
|
|
3859
|
+
if (!hasParticles.current) return scene;
|
|
3860
|
+
return injectParticles(scene, particleStateRef.current);
|
|
3861
|
+
}
|
|
3862
|
+
function injectParticles(scene, particleState) {
|
|
3863
|
+
const newRoot = injectParticlesInNode(scene.root, particleState);
|
|
3864
|
+
if (newRoot === scene.root) return scene;
|
|
3865
|
+
return { ...scene, root: newRoot };
|
|
3866
|
+
}
|
|
3867
|
+
function injectParticlesInNode(node, particleState) {
|
|
3868
|
+
if (node.type === "particles") {
|
|
3869
|
+
const particles = particleState.get(node.id);
|
|
3870
|
+
if (particles) {
|
|
3871
|
+
return { ...node, _particles: particles };
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
if (node.type === "group" && node.children) {
|
|
3875
|
+
const children = node.children;
|
|
3876
|
+
const newChildren = children.map((child) => injectParticlesInNode(child, particleState));
|
|
3877
|
+
const changed = newChildren.some((c, i) => c !== children[i]);
|
|
3878
|
+
if (changed) return { ...node, children: newChildren };
|
|
3879
|
+
}
|
|
3880
|
+
return node;
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// src/scene/helpers.ts
|
|
3884
|
+
function uid3() {
|
|
3885
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
3886
|
+
}
|
|
3887
|
+
function createScene(opts) {
|
|
3888
|
+
const w = opts?.width ?? 800;
|
|
3889
|
+
const h7 = opts?.height ?? 600;
|
|
3890
|
+
return {
|
|
3891
|
+
_sceneVersion: 1,
|
|
3892
|
+
root: { id: "root", type: "group", children: [] },
|
|
3893
|
+
camera: { x: w / 2, y: h7 / 2, zoom: 1 },
|
|
3894
|
+
background: opts?.background ?? "#1a1a2e",
|
|
3895
|
+
gradients: [],
|
|
3896
|
+
filters: [],
|
|
3897
|
+
width: w,
|
|
3898
|
+
height: h7
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function createNode(type, props) {
|
|
3902
|
+
return {
|
|
3903
|
+
id: props.id ?? uid3(),
|
|
3904
|
+
type,
|
|
3905
|
+
...props
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
function nodeById(scene, id) {
|
|
3909
|
+
return findNodeInGroup(scene.root, id);
|
|
3910
|
+
}
|
|
3911
|
+
function findNodeInGroup(node, id) {
|
|
3912
|
+
if (node.id === id) return node;
|
|
3913
|
+
if (node.type === "group" && node.children) {
|
|
3914
|
+
for (const child of node.children) {
|
|
3915
|
+
const found = findNodeInGroup(child, id);
|
|
3916
|
+
if (found) return found;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return null;
|
|
3920
|
+
}
|
|
3921
|
+
function findNodes(scene, predicate) {
|
|
3922
|
+
const results = [];
|
|
3923
|
+
walkNodes(scene.root, (node) => {
|
|
3924
|
+
if (predicate(node)) results.push(node);
|
|
3925
|
+
});
|
|
3926
|
+
return results;
|
|
3927
|
+
}
|
|
3928
|
+
function walkNodes(node, visitor) {
|
|
3929
|
+
visitor(node);
|
|
3930
|
+
if (node.type === "group" && node.children) {
|
|
3931
|
+
for (const child of node.children) {
|
|
3932
|
+
walkNodes(child, visitor);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
function allNodeIds(scene) {
|
|
3937
|
+
const ids = [];
|
|
3938
|
+
walkNodes(scene.root, (node) => ids.push(node.id));
|
|
3939
|
+
return ids;
|
|
3940
|
+
}
|
|
3941
|
+
function nodeCount(scene) {
|
|
3942
|
+
let count = 0;
|
|
3943
|
+
walkNodes(scene.root, () => count++);
|
|
3944
|
+
return count;
|
|
3945
|
+
}
|
|
3946
|
+
function cloneScene2(scene) {
|
|
3947
|
+
return JSON.parse(JSON.stringify(scene));
|
|
3948
|
+
}
|
|
3949
|
+
function removeNodeById(root, id) {
|
|
3950
|
+
if (!root.children) return false;
|
|
3951
|
+
const idx = root.children.findIndex((c) => c.id === id);
|
|
3952
|
+
if (idx !== -1) {
|
|
3953
|
+
root.children.splice(idx, 1);
|
|
3954
|
+
return true;
|
|
3955
|
+
}
|
|
3956
|
+
for (const child of root.children) {
|
|
3957
|
+
if (child.type === "group" && removeNodeById(child, id)) {
|
|
3958
|
+
return true;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return false;
|
|
3962
|
+
}
|
|
3963
|
+
function findParent(root, nodeId) {
|
|
3964
|
+
if (!root.children) return null;
|
|
3965
|
+
for (const child of root.children) {
|
|
3966
|
+
if (child.id === nodeId) return root;
|
|
3967
|
+
if (child.type === "group") {
|
|
3968
|
+
const parent = findParent(child, nodeId);
|
|
3969
|
+
if (parent) return parent;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
function sceneTools(z, namespace) {
|
|
3975
|
+
return createSceneTools(namespace ?? "scene", z);
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
// src/scene/path-builder.ts
|
|
3979
|
+
var PathBuilder = class _PathBuilder {
|
|
3980
|
+
commands = [];
|
|
3981
|
+
// ── Core commands ─────────────────────────────────────────────────────
|
|
3982
|
+
moveTo(x, y) {
|
|
3983
|
+
this.commands.push(`M ${x} ${y}`);
|
|
3984
|
+
return this;
|
|
3985
|
+
}
|
|
3986
|
+
lineTo(x, y) {
|
|
3987
|
+
this.commands.push(`L ${x} ${y}`);
|
|
3988
|
+
return this;
|
|
3989
|
+
}
|
|
3990
|
+
horizontalTo(x) {
|
|
3991
|
+
this.commands.push(`H ${x}`);
|
|
3992
|
+
return this;
|
|
3993
|
+
}
|
|
3994
|
+
verticalTo(y) {
|
|
3995
|
+
this.commands.push(`V ${y}`);
|
|
3996
|
+
return this;
|
|
3997
|
+
}
|
|
3998
|
+
quadTo(cx, cy, x, y) {
|
|
3999
|
+
this.commands.push(`Q ${cx} ${cy} ${x} ${y}`);
|
|
4000
|
+
return this;
|
|
4001
|
+
}
|
|
4002
|
+
cubicTo(c1x, c1y, c2x, c2y, x, y) {
|
|
4003
|
+
this.commands.push(`C ${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y}`);
|
|
4004
|
+
return this;
|
|
4005
|
+
}
|
|
4006
|
+
arcTo(rx, ry, rotation, largeArc, sweep, x, y) {
|
|
4007
|
+
this.commands.push(`A ${rx} ${ry} ${rotation} ${largeArc ? 1 : 0} ${sweep ? 1 : 0} ${x} ${y}`);
|
|
4008
|
+
return this;
|
|
4009
|
+
}
|
|
4010
|
+
close() {
|
|
4011
|
+
this.commands.push("Z");
|
|
4012
|
+
return this;
|
|
4013
|
+
}
|
|
4014
|
+
// ── Higher-level helpers ──────────────────────────────────────────────
|
|
4015
|
+
rect(x, y, w, h7) {
|
|
4016
|
+
return this.moveTo(x, y).lineTo(x + w, y).lineTo(x + w, y + h7).lineTo(x, y + h7).close();
|
|
4017
|
+
}
|
|
4018
|
+
roundedRect(x, y, w, h7, rx, ry) {
|
|
4019
|
+
const r = ry ?? rx;
|
|
4020
|
+
return this.moveTo(x + rx, y).lineTo(x + w - rx, y).arcTo(rx, r, 0, false, true, x + w, y + r).lineTo(x + w, y + h7 - r).arcTo(rx, r, 0, false, true, x + w - rx, y + h7).lineTo(x + rx, y + h7).arcTo(rx, r, 0, false, true, x, y + h7 - r).lineTo(x, y + r).arcTo(rx, r, 0, false, true, x + rx, y).close();
|
|
4021
|
+
}
|
|
4022
|
+
circle(cx, cy, r) {
|
|
4023
|
+
return this.moveTo(cx - r, cy).arcTo(r, r, 0, true, true, cx + r, cy).arcTo(r, r, 0, true, true, cx - r, cy).close();
|
|
4024
|
+
}
|
|
4025
|
+
ellipse(cx, cy, rx, ry) {
|
|
4026
|
+
return this.moveTo(cx - rx, cy).arcTo(rx, ry, 0, true, true, cx + rx, cy).arcTo(rx, ry, 0, true, true, cx - rx, cy).close();
|
|
4027
|
+
}
|
|
4028
|
+
star(cx, cy, points, outerR, innerR) {
|
|
4029
|
+
const step = Math.PI / points;
|
|
4030
|
+
for (let i = 0; i < 2 * points; i++) {
|
|
4031
|
+
const angle = i * step - Math.PI / 2;
|
|
4032
|
+
const r = i % 2 === 0 ? outerR : innerR;
|
|
4033
|
+
const x = cx + r * Math.cos(angle);
|
|
4034
|
+
const y = cy + r * Math.sin(angle);
|
|
4035
|
+
if (i === 0) {
|
|
4036
|
+
this.moveTo(x, y);
|
|
4037
|
+
} else {
|
|
4038
|
+
this.lineTo(x, y);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
return this.close();
|
|
4042
|
+
}
|
|
4043
|
+
arrow(x1, y1, x2, y2, headSize = 10) {
|
|
4044
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
4045
|
+
const ha1 = angle + Math.PI * 0.8;
|
|
4046
|
+
const ha2 = angle - Math.PI * 0.8;
|
|
4047
|
+
return this.moveTo(x1, y1).lineTo(x2, y2).moveTo(x2, y2).lineTo(x2 + headSize * Math.cos(ha1), y2 + headSize * Math.sin(ha1)).moveTo(x2, y2).lineTo(x2 + headSize * Math.cos(ha2), y2 + headSize * Math.sin(ha2));
|
|
4048
|
+
}
|
|
4049
|
+
// ── Output ────────────────────────────────────────────────────────────
|
|
4050
|
+
build() {
|
|
4051
|
+
return this.commands.join(" ");
|
|
4052
|
+
}
|
|
4053
|
+
static from() {
|
|
4054
|
+
return new _PathBuilder();
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
|
|
4058
|
+
// src/scene/rules.ts
|
|
4059
|
+
function getReact6() {
|
|
4060
|
+
const R = globalThis.React;
|
|
4061
|
+
if (!R) throw new Error("React not available");
|
|
4062
|
+
return R;
|
|
4063
|
+
}
|
|
4064
|
+
var React3 = new Proxy({}, {
|
|
4065
|
+
get(_target, prop) {
|
|
4066
|
+
return getReact6()[prop];
|
|
4067
|
+
}
|
|
4068
|
+
});
|
|
4069
|
+
function nodeMatchesSelector(node, selector) {
|
|
4070
|
+
const s = selector.trim();
|
|
4071
|
+
if (s === "*") return !!node.data?.entityType;
|
|
4072
|
+
const colon = s.indexOf(":");
|
|
4073
|
+
if (colon === -1) return false;
|
|
4074
|
+
const prefix = s.slice(0, colon);
|
|
4075
|
+
const value = s.slice(colon + 1);
|
|
4076
|
+
switch (prefix) {
|
|
4077
|
+
case "entityType":
|
|
4078
|
+
return node.data?.entityType === value;
|
|
4079
|
+
case "tag": {
|
|
4080
|
+
const tags = node.data?.tags;
|
|
4081
|
+
return Array.isArray(tags) && tags.includes(value);
|
|
4082
|
+
}
|
|
4083
|
+
case "name":
|
|
4084
|
+
return node.name === value;
|
|
4085
|
+
case "type":
|
|
4086
|
+
return node.type === value;
|
|
4087
|
+
default:
|
|
4088
|
+
return false;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
function dist(a, b) {
|
|
4092
|
+
const dx = a.x - b.x;
|
|
4093
|
+
const dy = a.y - b.y;
|
|
4094
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
4095
|
+
}
|
|
4096
|
+
function nodePos(node) {
|
|
4097
|
+
return { x: node.transform?.x ?? 0, y: node.transform?.y ?? 0 };
|
|
4098
|
+
}
|
|
4099
|
+
function applyVariance(v, variance) {
|
|
4100
|
+
if (variance <= 0) return v;
|
|
4101
|
+
return v * (1 + (Math.random() * 2 - 1) * variance);
|
|
4102
|
+
}
|
|
4103
|
+
function collectNodes(root) {
|
|
4104
|
+
const out = [];
|
|
4105
|
+
walkNodes(root, (n) => out.push(n));
|
|
4106
|
+
return out;
|
|
4107
|
+
}
|
|
4108
|
+
function checkCondition(rule, node, allNodes, now, cooldowns) {
|
|
4109
|
+
const cond = rule.condition;
|
|
4110
|
+
if (!nodeMatchesSelector(node, cond.selector)) return false;
|
|
4111
|
+
if (cond.state) {
|
|
4112
|
+
for (const [k, v] of Object.entries(cond.state)) {
|
|
4113
|
+
if (node.data?.[k] !== v) return false;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
if (cond.proximity) {
|
|
4117
|
+
const pos = nodePos(node);
|
|
4118
|
+
const inRange = allNodes.some(
|
|
4119
|
+
(n) => n.id !== node.id && nodeMatchesSelector(n, cond.proximity.target) && dist(pos, nodePos(n)) <= cond.proximity.distance
|
|
4120
|
+
);
|
|
4121
|
+
if (!inRange) return false;
|
|
4122
|
+
}
|
|
4123
|
+
if (cond.cooldownMs) {
|
|
4124
|
+
const perNode = cooldowns.get(rule.id);
|
|
4125
|
+
const last = perNode?.[node.id] ?? 0;
|
|
4126
|
+
if (now - last < cond.cooldownMs) return false;
|
|
4127
|
+
}
|
|
4128
|
+
if (cond.probability != null && Math.random() > cond.probability) return false;
|
|
4129
|
+
return true;
|
|
4130
|
+
}
|
|
4131
|
+
function applyEffect(rule, node, pending) {
|
|
4132
|
+
const eff = rule.effect;
|
|
4133
|
+
if (eff.probability != null && Math.random() > eff.probability) return null;
|
|
4134
|
+
const variance = eff.variance ?? 0;
|
|
4135
|
+
let modified = null;
|
|
4136
|
+
switch (eff.type) {
|
|
4137
|
+
case "transform": {
|
|
4138
|
+
const t = { ...node.transform ?? {} };
|
|
4139
|
+
let changed = false;
|
|
4140
|
+
if (eff.dx != null) {
|
|
4141
|
+
t.x = (t.x ?? 0) + applyVariance(eff.dx, variance);
|
|
4142
|
+
changed = true;
|
|
4143
|
+
}
|
|
4144
|
+
if (eff.dy != null) {
|
|
4145
|
+
t.y = (t.y ?? 0) + applyVariance(eff.dy, variance);
|
|
4146
|
+
changed = true;
|
|
4147
|
+
}
|
|
4148
|
+
if (eff.dRotation != null) {
|
|
4149
|
+
t.rotation = (t.rotation ?? 0) + applyVariance(eff.dRotation, variance);
|
|
4150
|
+
changed = true;
|
|
4151
|
+
}
|
|
4152
|
+
if (changed) modified = { ...node, transform: t };
|
|
4153
|
+
break;
|
|
4154
|
+
}
|
|
4155
|
+
case "style": {
|
|
4156
|
+
if (eff.styleUpdates) {
|
|
4157
|
+
modified = {
|
|
4158
|
+
...node,
|
|
4159
|
+
style: { ...node.style ?? {}, ...eff.styleUpdates }
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
break;
|
|
4163
|
+
}
|
|
4164
|
+
case "data": {
|
|
4165
|
+
if (eff.dataUpdates) {
|
|
4166
|
+
modified = {
|
|
4167
|
+
...node,
|
|
4168
|
+
data: { ...node.data ?? {}, ...eff.dataUpdates }
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
break;
|
|
4172
|
+
}
|
|
4173
|
+
case "counter": {
|
|
4174
|
+
if (eff.field && eff.delta != null) {
|
|
4175
|
+
const cur = node.data?.[eff.field] ?? 0;
|
|
4176
|
+
const delta = applyVariance(eff.delta, variance);
|
|
4177
|
+
modified = {
|
|
4178
|
+
...node,
|
|
4179
|
+
data: { ...node.data ?? {}, [eff.field]: cur + delta }
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
break;
|
|
4183
|
+
}
|
|
4184
|
+
case "tween": {
|
|
4185
|
+
if (eff.tween) {
|
|
4186
|
+
modified = {
|
|
4187
|
+
...node,
|
|
4188
|
+
tween: { ...eff.tween, startedAt: Date.now() }
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
break;
|
|
4192
|
+
}
|
|
4193
|
+
case "spawn": {
|
|
4194
|
+
if (eff.spawnNode) {
|
|
4195
|
+
pending.push({
|
|
4196
|
+
op: "spawn",
|
|
4197
|
+
node: { ...eff.spawnNode },
|
|
4198
|
+
parentPos: nodePos(node)
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
4201
|
+
break;
|
|
4202
|
+
}
|
|
4203
|
+
case "remove": {
|
|
4204
|
+
pending.push({ op: "remove", nodeId: node.id });
|
|
4205
|
+
break;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
return modified;
|
|
4209
|
+
}
|
|
4210
|
+
function replaceNode(group, id, replacement) {
|
|
4211
|
+
if (!group.children) return false;
|
|
4212
|
+
for (let i = 0; i < group.children.length; i++) {
|
|
4213
|
+
if (group.children[i].id === id) {
|
|
4214
|
+
group.children[i] = replacement;
|
|
4215
|
+
return true;
|
|
4216
|
+
}
|
|
4217
|
+
if (group.children[i].type === "group" && replaceNode(group.children[i], id, replacement)) {
|
|
4218
|
+
return true;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return false;
|
|
4222
|
+
}
|
|
4223
|
+
function useRuleTick(scene, rules, worldMeta, callTool) {
|
|
4224
|
+
const [simScene, setSimScene] = React3.useState(scene);
|
|
4225
|
+
const [stats, setStats] = React3.useState({
|
|
4226
|
+
rulesEvaluated: 0,
|
|
4227
|
+
rulesFired: 0,
|
|
4228
|
+
nodesAffected: 0,
|
|
4229
|
+
ticksElapsed: 0
|
|
4230
|
+
});
|
|
4231
|
+
const rafRef = React3.useRef(null);
|
|
4232
|
+
const lastTickRef = React3.useRef(0);
|
|
4233
|
+
const tickCountRef = React3.useRef(0);
|
|
4234
|
+
const cooldownsRef = React3.useRef(/* @__PURE__ */ new Map());
|
|
4235
|
+
const pendingOpsRef = React3.useRef([]);
|
|
4236
|
+
const batchTimerRef = React3.useRef(null);
|
|
4237
|
+
const flushPending = React3.useCallback(() => {
|
|
4238
|
+
const ops = pendingOpsRef.current;
|
|
4239
|
+
if (ops.length === 0) return;
|
|
4240
|
+
pendingOpsRef.current = [];
|
|
4241
|
+
const batchOps = [];
|
|
4242
|
+
for (const op of ops) {
|
|
4243
|
+
if (op.op === "spawn") {
|
|
4244
|
+
const id = Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
4245
|
+
const spawnX = op.parentPos.x + (op.node.spawnOffset?.x ?? (Math.random() - 0.5) * 60);
|
|
4246
|
+
const spawnY = op.parentPos.y + (op.node.spawnOffset?.y ?? (Math.random() - 0.5) * 60);
|
|
4247
|
+
const { spawnOffset: _so, ...nodeProps } = op.node;
|
|
4248
|
+
batchOps.push({
|
|
4249
|
+
op: "add",
|
|
4250
|
+
node: {
|
|
4251
|
+
...nodeProps,
|
|
4252
|
+
id,
|
|
4253
|
+
transform: {
|
|
4254
|
+
...nodeProps.transform ?? {},
|
|
4255
|
+
x: spawnX,
|
|
4256
|
+
y: spawnY
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
});
|
|
4260
|
+
} else if (op.op === "remove") {
|
|
4261
|
+
batchOps.push({ op: "remove", nodeIds: [op.nodeId] });
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
if (batchOps.length > 0) {
|
|
4265
|
+
callTool("scene.batch", { operations: batchOps }).catch(() => {
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
}, [callTool]);
|
|
4269
|
+
const scheduleBatchFlush = React3.useCallback(() => {
|
|
4270
|
+
if (batchTimerRef.current) return;
|
|
4271
|
+
batchTimerRef.current = setTimeout(() => {
|
|
4272
|
+
batchTimerRef.current = null;
|
|
4273
|
+
flushPending();
|
|
4274
|
+
}, 300);
|
|
4275
|
+
}, [flushPending]);
|
|
4276
|
+
React3.useEffect(() => {
|
|
4277
|
+
setSimScene(scene);
|
|
4278
|
+
}, [scene]);
|
|
4279
|
+
React3.useEffect(() => {
|
|
4280
|
+
if (worldMeta.paused) {
|
|
4281
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
4282
|
+
rafRef.current = null;
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
const tickRules = rules.filter(
|
|
4286
|
+
(r) => r.enabled && r.trigger === "tick"
|
|
4287
|
+
);
|
|
4288
|
+
if (tickRules.length === 0) {
|
|
4289
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
4290
|
+
rafRef.current = null;
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
const tick = (now) => {
|
|
4294
|
+
if (now - lastTickRef.current < worldMeta.tickSpeed) {
|
|
4295
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
lastTickRef.current = now;
|
|
4299
|
+
tickCountRef.current++;
|
|
4300
|
+
setSimScene((prevScene) => {
|
|
4301
|
+
const working = cloneScene2(prevScene);
|
|
4302
|
+
const allNodes = collectNodes(working.root);
|
|
4303
|
+
const pending = [];
|
|
4304
|
+
let rulesEvaluated = 0;
|
|
4305
|
+
let rulesFired = 0;
|
|
4306
|
+
let nodesAffected = 0;
|
|
4307
|
+
for (const rule of tickRules) {
|
|
4308
|
+
if (!cooldownsRef.current.has(rule.id)) {
|
|
4309
|
+
cooldownsRef.current.set(rule.id, {});
|
|
4310
|
+
}
|
|
4311
|
+
rulesEvaluated++;
|
|
4312
|
+
let fired = false;
|
|
4313
|
+
for (const node of allNodes) {
|
|
4314
|
+
if (checkCondition(rule, node, allNodes, now, cooldownsRef.current)) {
|
|
4315
|
+
const modified = applyEffect(rule, node, pending);
|
|
4316
|
+
if (modified) {
|
|
4317
|
+
replaceNode(working.root, node.id, modified);
|
|
4318
|
+
nodesAffected++;
|
|
4319
|
+
fired = true;
|
|
4320
|
+
const cd = cooldownsRef.current.get(rule.id);
|
|
4321
|
+
cd[node.id] = now;
|
|
4322
|
+
} else if (rule.effect.type === "spawn" || rule.effect.type === "remove") {
|
|
4323
|
+
fired = true;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
if (fired) rulesFired++;
|
|
4328
|
+
}
|
|
4329
|
+
if (pending.length > 0) {
|
|
4330
|
+
pendingOpsRef.current.push(...pending);
|
|
4331
|
+
scheduleBatchFlush();
|
|
4332
|
+
}
|
|
4333
|
+
setStats({
|
|
4334
|
+
rulesEvaluated,
|
|
4335
|
+
rulesFired,
|
|
4336
|
+
nodesAffected,
|
|
4337
|
+
ticksElapsed: tickCountRef.current
|
|
4338
|
+
});
|
|
4339
|
+
return working;
|
|
4340
|
+
});
|
|
4341
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
4342
|
+
};
|
|
4343
|
+
lastTickRef.current = performance.now();
|
|
4344
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
4345
|
+
return () => {
|
|
4346
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
4347
|
+
};
|
|
4348
|
+
}, [rules, worldMeta.paused, worldMeta.tickSpeed, scheduleBatchFlush]);
|
|
4349
|
+
React3.useEffect(() => {
|
|
4350
|
+
return () => {
|
|
4351
|
+
if (batchTimerRef.current) clearTimeout(batchTimerRef.current);
|
|
4352
|
+
};
|
|
4353
|
+
}, []);
|
|
4354
|
+
return { simulatedScene: simScene, stats };
|
|
4355
|
+
}
|
|
4356
|
+
function createRuleTools(z) {
|
|
4357
|
+
return [
|
|
4358
|
+
// ── _rules.set ──────────────────────────────────────────────
|
|
4359
|
+
{
|
|
4360
|
+
name: "_rules.set",
|
|
4361
|
+
description: `Create or update a simulation rule. Rules run client-side at tick speed (~10/sec) for emergent behavior.
|
|
4362
|
+
|
|
4363
|
+
Entity convention: scene nodes with data.entityType and data.tags are "entities" that rules can target.
|
|
4364
|
+
|
|
4365
|
+
Selector syntax:
|
|
4366
|
+
"entityType:fish" \u2014 matches nodes where data.entityType === "fish"
|
|
4367
|
+
"tag:alive" \u2014 matches nodes where data.tags includes "alive"
|
|
4368
|
+
"name:hero" \u2014 matches nodes where name === "hero"
|
|
4369
|
+
"type:circle" \u2014 matches nodes where type === "circle"
|
|
4370
|
+
"*" \u2014 matches any node with data.entityType
|
|
4371
|
+
|
|
4372
|
+
Effect types:
|
|
4373
|
+
transform \u2014 move/rotate nodes each tick (dx, dy, dRotation)
|
|
4374
|
+
style \u2014 update visual style (styleUpdates)
|
|
4375
|
+
data \u2014 update node metadata (dataUpdates)
|
|
4376
|
+
counter \u2014 increment/decrement a data field (field, delta)
|
|
4377
|
+
spawn \u2014 create new nodes near matched nodes (spawnNode)
|
|
4378
|
+
remove \u2014 delete matched nodes
|
|
4379
|
+
tween \u2014 start a tween animation on matched nodes
|
|
4380
|
+
|
|
4381
|
+
Example \u2014 fish swim right:
|
|
4382
|
+
{ id: "fish-swim", name: "Fish Swim", description: "Fish drift right", enabled: true,
|
|
4383
|
+
trigger: "tick", condition: { selector: "entityType:fish" },
|
|
4384
|
+
effect: { type: "transform", dx: 2, variance: 0.3 } }
|
|
4385
|
+
|
|
4386
|
+
Example \u2014 predator eats prey:
|
|
4387
|
+
{ id: "predator-eat", name: "Predator Eats", description: "Remove prey near predator", enabled: true,
|
|
4388
|
+
trigger: "tick", condition: { selector: "entityType:prey", proximity: { target: "entityType:predator", distance: 30 } },
|
|
4389
|
+
effect: { type: "remove" } }`,
|
|
4390
|
+
input_schema: z.object({
|
|
4391
|
+
id: z.string().describe("Unique rule ID"),
|
|
4392
|
+
name: z.string().describe("Human-readable name"),
|
|
4393
|
+
description: z.string().optional().describe("What this rule does"),
|
|
4394
|
+
enabled: z.boolean().optional().describe("Whether rule is active (default true)"),
|
|
4395
|
+
trigger: z.enum(["tick", "interaction", "proximity", "timer"]).optional().describe("When to evaluate (default tick)"),
|
|
4396
|
+
condition: z.object({
|
|
4397
|
+
selector: z.string().describe("Entity selector: entityType:X, tag:X, name:X, type:X, or *"),
|
|
4398
|
+
proximity: z.object({
|
|
4399
|
+
target: z.string().describe("Selector for proximity target"),
|
|
4400
|
+
distance: z.number().describe("Max distance in pixels")
|
|
4401
|
+
}).optional(),
|
|
4402
|
+
state: z.record(z.any()).optional().describe("Match nodes where data[key] === value"),
|
|
4403
|
+
cooldownMs: z.number().optional().describe("Minimum ms between firings per node"),
|
|
4404
|
+
probability: z.number().min(0).max(1).optional().describe("Chance of evaluating (0-1)")
|
|
4405
|
+
}),
|
|
4406
|
+
effect: z.object({
|
|
4407
|
+
type: z.enum(["transform", "style", "data", "counter", "spawn", "remove", "tween"]),
|
|
4408
|
+
dx: z.number().optional().describe("X movement per tick"),
|
|
4409
|
+
dy: z.number().optional().describe("Y movement per tick"),
|
|
4410
|
+
dRotation: z.number().optional().describe("Rotation per tick (degrees)"),
|
|
4411
|
+
styleUpdates: z.record(z.any()).optional().describe("Style properties to set"),
|
|
4412
|
+
dataUpdates: z.record(z.any()).optional().describe("Data properties to set"),
|
|
4413
|
+
field: z.string().optional().describe("Counter field name"),
|
|
4414
|
+
delta: z.number().optional().describe("Counter increment per tick"),
|
|
4415
|
+
spawnNode: z.record(z.any()).optional().describe("Node template to spawn"),
|
|
4416
|
+
spawnOffset: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
4417
|
+
tween: z.object({
|
|
4418
|
+
property: z.string(),
|
|
4419
|
+
from: z.number(),
|
|
4420
|
+
to: z.number(),
|
|
4421
|
+
duration: z.number(),
|
|
4422
|
+
easing: z.string().optional(),
|
|
4423
|
+
repeat: z.number().optional(),
|
|
4424
|
+
yoyo: z.boolean().optional()
|
|
4425
|
+
}).optional(),
|
|
4426
|
+
variance: z.number().min(0).max(1).optional().describe("Random variance (0-1)"),
|
|
4427
|
+
probability: z.number().min(0).max(1).optional().describe("Chance effect fires (0-1)")
|
|
4428
|
+
})
|
|
4429
|
+
}),
|
|
4430
|
+
risk: "low",
|
|
4431
|
+
capabilities_required: ["state.write"],
|
|
4432
|
+
handler: async (ctx, input) => {
|
|
4433
|
+
const rules = [...ctx.state._rules || []];
|
|
4434
|
+
const rule = {
|
|
4435
|
+
id: input.id,
|
|
4436
|
+
name: input.name,
|
|
4437
|
+
description: input.description ?? "",
|
|
4438
|
+
enabled: input.enabled ?? true,
|
|
4439
|
+
trigger: input.trigger ?? "tick",
|
|
4440
|
+
condition: input.condition,
|
|
4441
|
+
effect: input.effect
|
|
4442
|
+
};
|
|
4443
|
+
const idx = rules.findIndex((r) => r.id === input.id);
|
|
4444
|
+
if (idx !== -1) {
|
|
4445
|
+
rules[idx] = rule;
|
|
4446
|
+
} else {
|
|
4447
|
+
rules.push(rule);
|
|
4448
|
+
}
|
|
4449
|
+
ctx.setState({ ...ctx.state, _rules: rules });
|
|
4450
|
+
return { ruleId: rule.id, total: rules.length, action: idx !== -1 ? "updated" : "created" };
|
|
4451
|
+
}
|
|
4452
|
+
},
|
|
4453
|
+
// ── _rules.remove ───────────────────────────────────────────
|
|
4454
|
+
{
|
|
4455
|
+
name: "_rules.remove",
|
|
4456
|
+
description: "Remove a simulation rule by ID.",
|
|
4457
|
+
input_schema: z.object({
|
|
4458
|
+
id: z.string().describe("ID of the rule to remove")
|
|
4459
|
+
}),
|
|
4460
|
+
risk: "low",
|
|
4461
|
+
capabilities_required: ["state.write"],
|
|
4462
|
+
handler: async (ctx, input) => {
|
|
4463
|
+
const rules = (ctx.state._rules || []).filter(
|
|
4464
|
+
(r) => r.id !== input.id
|
|
4465
|
+
);
|
|
4466
|
+
ctx.setState({ ...ctx.state, _rules: rules });
|
|
4467
|
+
return { removed: input.id, remaining: rules.length };
|
|
4468
|
+
}
|
|
4469
|
+
},
|
|
4470
|
+
// ── _rules.world ────────────────────────────────────────────
|
|
4471
|
+
{
|
|
4472
|
+
name: "_rules.world",
|
|
4473
|
+
description: `Set world metadata \u2014 name, description, paused state, tick speed.
|
|
4474
|
+
|
|
4475
|
+
Examples:
|
|
4476
|
+
- Name the world: { name: "The Reef", description: "An underwater ecosystem" }
|
|
4477
|
+
- Pause: { paused: true }
|
|
4478
|
+
- Speed up: { tickSpeed: 50 }
|
|
4479
|
+
- Slow down: { tickSpeed: 200 }`,
|
|
4480
|
+
input_schema: z.object({
|
|
4481
|
+
name: z.string().optional().describe("World name"),
|
|
4482
|
+
description: z.string().optional().describe("World description"),
|
|
4483
|
+
paused: z.boolean().optional().describe("Pause/resume simulation"),
|
|
4484
|
+
tickSpeed: z.number().min(16).max(2e3).optional().describe("Ms between ticks (16=60fps, 100=10fps, default 100)")
|
|
4485
|
+
}),
|
|
4486
|
+
risk: "low",
|
|
4487
|
+
capabilities_required: ["state.write"],
|
|
4488
|
+
handler: async (ctx, input) => {
|
|
4489
|
+
const meta = {
|
|
4490
|
+
...ctx.state._worldMeta || {
|
|
4491
|
+
name: "Untitled",
|
|
4492
|
+
description: "",
|
|
4493
|
+
paused: false,
|
|
4494
|
+
tickSpeed: 100
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
if (input.name != null) meta.name = input.name;
|
|
4498
|
+
if (input.description != null) meta.description = input.description;
|
|
4499
|
+
if (input.paused != null) meta.paused = input.paused;
|
|
4500
|
+
if (input.tickSpeed != null) meta.tickSpeed = input.tickSpeed;
|
|
4501
|
+
ctx.setState({ ...ctx.state, _worldMeta: meta });
|
|
4502
|
+
return { worldMeta: meta };
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
];
|
|
4506
|
+
}
|
|
4507
|
+
function ruleTools(z) {
|
|
4508
|
+
return createRuleTools(z);
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// src/chat.ts
|
|
4512
|
+
function uid4() {
|
|
4513
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
4514
|
+
}
|
|
4515
|
+
function capMessages2(msgs, max = 200) {
|
|
4516
|
+
return msgs.length > max ? msgs.slice(-max) : msgs;
|
|
4517
|
+
}
|
|
4518
|
+
function getReact7() {
|
|
4519
|
+
const R = globalThis.React;
|
|
4520
|
+
if (!R) throw new Error("React is not available.");
|
|
4521
|
+
return R;
|
|
4522
|
+
}
|
|
4523
|
+
function h4(type, props, ...children) {
|
|
4524
|
+
return getReact7().createElement(type, props, ...children);
|
|
4525
|
+
}
|
|
4526
|
+
function createChatTools(z) {
|
|
4527
|
+
return [
|
|
4528
|
+
{
|
|
4529
|
+
name: "_chat.send",
|
|
4530
|
+
description: "Send a chat message",
|
|
4531
|
+
input_schema: z.object({
|
|
4532
|
+
message: z.string().min(1).max(2e3),
|
|
4533
|
+
replyTo: z.string().optional()
|
|
4534
|
+
}),
|
|
4535
|
+
risk: "low",
|
|
4536
|
+
capabilities_required: ["state.write"],
|
|
4537
|
+
handler: async (ctx, input) => {
|
|
4538
|
+
const messages = capMessages2([
|
|
4539
|
+
...ctx.state._chat || [],
|
|
4540
|
+
{
|
|
4541
|
+
id: uid4(),
|
|
4542
|
+
actorId: ctx.actorId,
|
|
4543
|
+
message: input.message,
|
|
4544
|
+
replyTo: input.replyTo,
|
|
4545
|
+
ts: ctx.timestamp
|
|
4546
|
+
}
|
|
4547
|
+
]);
|
|
4548
|
+
ctx.setState({ ...ctx.state, _chat: messages });
|
|
4549
|
+
return { sent: true, messageCount: messages.length };
|
|
4550
|
+
}
|
|
4551
|
+
},
|
|
4552
|
+
{
|
|
4553
|
+
name: "_chat.clear",
|
|
4554
|
+
description: "Clear all chat messages",
|
|
4555
|
+
input_schema: z.object({}),
|
|
4556
|
+
risk: "medium",
|
|
4557
|
+
capabilities_required: ["state.write"],
|
|
4558
|
+
handler: async (ctx) => {
|
|
4559
|
+
ctx.setState({ ...ctx.state, _chat: [] });
|
|
4560
|
+
return { cleared: true };
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
];
|
|
4564
|
+
}
|
|
4565
|
+
function useChat(sharedState, callTool, actorId, ephemeralState, setEphemeral) {
|
|
4566
|
+
const React4 = getReact7();
|
|
4567
|
+
const messages = sharedState._chat || [];
|
|
4568
|
+
const sendMessage = React4.useCallback(
|
|
4569
|
+
async (message, replyTo) => {
|
|
4570
|
+
await callTool("_chat.send", { message, replyTo });
|
|
4571
|
+
},
|
|
4572
|
+
[callTool]
|
|
4573
|
+
);
|
|
4574
|
+
const clearChat = React4.useCallback(async () => {
|
|
4575
|
+
await callTool("_chat.clear", {});
|
|
4576
|
+
}, [callTool]);
|
|
4577
|
+
const { setTyping, typingUsers } = useTypingIndicator(actorId, ephemeralState, setEphemeral);
|
|
4578
|
+
return { messages, sendMessage, clearChat, setTyping, typingUsers };
|
|
4579
|
+
}
|
|
4580
|
+
function parseActorId(id) {
|
|
4581
|
+
const m = id.match(/^(.+)-(human|ai)-(\d+)$/);
|
|
4582
|
+
if (m) return { username: m[1], type: m[2] };
|
|
4583
|
+
return { username: id, type: "unknown" };
|
|
4584
|
+
}
|
|
4585
|
+
function formatTime(ts) {
|
|
4586
|
+
const d = new Date(ts);
|
|
4587
|
+
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
4588
|
+
}
|
|
4589
|
+
function ChatPanel({
|
|
4590
|
+
sharedState,
|
|
4591
|
+
callTool,
|
|
4592
|
+
actorId,
|
|
4593
|
+
ephemeralState,
|
|
4594
|
+
setEphemeral,
|
|
4595
|
+
style,
|
|
4596
|
+
embedded
|
|
4597
|
+
}) {
|
|
4598
|
+
const React4 = getReact7();
|
|
4599
|
+
const [open, setOpen] = React4.useState(false);
|
|
4600
|
+
const [inputValue, setInputValue] = React4.useState("");
|
|
4601
|
+
const [sending, setSending] = React4.useState(false);
|
|
4602
|
+
const [lastSeenCount, setLastSeenCount] = React4.useState(0);
|
|
4603
|
+
const listRef = React4.useRef(null);
|
|
4604
|
+
const { messages, sendMessage, setTyping, typingUsers } = useChat(
|
|
4605
|
+
sharedState,
|
|
4606
|
+
callTool,
|
|
4607
|
+
actorId,
|
|
4608
|
+
ephemeralState,
|
|
4609
|
+
setEphemeral
|
|
4610
|
+
);
|
|
4611
|
+
const unread = open ? 0 : Math.max(0, messages.length - lastSeenCount);
|
|
4612
|
+
React4.useEffect(() => {
|
|
4613
|
+
if (open || embedded) {
|
|
4614
|
+
setLastSeenCount(messages.length);
|
|
4615
|
+
}
|
|
4616
|
+
}, [open, embedded, messages.length]);
|
|
4617
|
+
React4.useEffect(() => {
|
|
4618
|
+
if (listRef.current) {
|
|
4619
|
+
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
4620
|
+
}
|
|
4621
|
+
}, [messages.length]);
|
|
4622
|
+
const handleSend = React4.useCallback(async () => {
|
|
4623
|
+
const text = inputValue.trim();
|
|
4624
|
+
if (!text || sending) return;
|
|
4625
|
+
setSending(true);
|
|
4626
|
+
setInputValue("");
|
|
4627
|
+
setTyping(false);
|
|
4628
|
+
try {
|
|
4629
|
+
await sendMessage(text);
|
|
4630
|
+
} catch {
|
|
4631
|
+
} finally {
|
|
4632
|
+
setSending(false);
|
|
4633
|
+
}
|
|
4634
|
+
}, [inputValue, sending, sendMessage, setTyping]);
|
|
4635
|
+
const handleKeyDown = React4.useCallback(
|
|
4636
|
+
(e) => {
|
|
4637
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
4638
|
+
e.preventDefault();
|
|
4639
|
+
handleSend();
|
|
4640
|
+
}
|
|
4641
|
+
},
|
|
4642
|
+
[handleSend]
|
|
4643
|
+
);
|
|
4644
|
+
const handleInputChange = React4.useCallback(
|
|
4645
|
+
(e) => {
|
|
4646
|
+
setInputValue(e.target.value);
|
|
4647
|
+
setTyping(e.target.value.length > 0);
|
|
4648
|
+
},
|
|
4649
|
+
[setTyping]
|
|
4650
|
+
);
|
|
4651
|
+
const actorColors = {
|
|
4652
|
+
human: "#60a5fa",
|
|
4653
|
+
ai: "#a78bfa",
|
|
4654
|
+
unknown: "#94a3b8"
|
|
4655
|
+
};
|
|
4656
|
+
const messageList = h4(
|
|
4657
|
+
"div",
|
|
4658
|
+
{
|
|
4659
|
+
ref: listRef,
|
|
4660
|
+
style: {
|
|
4661
|
+
flex: 1,
|
|
4662
|
+
overflowY: "auto",
|
|
4663
|
+
padding: "8px 12px",
|
|
4664
|
+
display: "flex",
|
|
4665
|
+
flexDirection: "column",
|
|
4666
|
+
gap: "6px",
|
|
4667
|
+
...embedded ? {} : { minHeight: "200px", maxHeight: "340px" }
|
|
4668
|
+
}
|
|
4669
|
+
},
|
|
4670
|
+
messages.length === 0 ? h4(
|
|
4671
|
+
"div",
|
|
4672
|
+
{
|
|
4673
|
+
style: {
|
|
4674
|
+
color: "#4a4a5a",
|
|
4675
|
+
fontSize: "13px",
|
|
4676
|
+
textAlign: "center",
|
|
4677
|
+
padding: "32px 0"
|
|
4678
|
+
}
|
|
4679
|
+
},
|
|
4680
|
+
"No messages yet"
|
|
4681
|
+
) : messages.map((msg) => {
|
|
4682
|
+
const { username, type } = parseActorId(msg.actorId);
|
|
4683
|
+
const isMe = msg.actorId === actorId;
|
|
4684
|
+
return h4(
|
|
4685
|
+
"div",
|
|
4686
|
+
{
|
|
4687
|
+
key: msg.id,
|
|
4688
|
+
style: {
|
|
4689
|
+
display: "flex",
|
|
4690
|
+
flexDirection: "column",
|
|
4691
|
+
alignItems: isMe ? "flex-end" : "flex-start"
|
|
4692
|
+
}
|
|
4693
|
+
},
|
|
4694
|
+
h4(
|
|
4695
|
+
"div",
|
|
4696
|
+
{
|
|
4697
|
+
style: {
|
|
4698
|
+
display: "flex",
|
|
4699
|
+
gap: "6px",
|
|
4700
|
+
alignItems: "baseline",
|
|
4701
|
+
flexDirection: isMe ? "row-reverse" : "row"
|
|
4702
|
+
}
|
|
4703
|
+
},
|
|
4704
|
+
h4(
|
|
4705
|
+
"span",
|
|
4706
|
+
{
|
|
4707
|
+
style: {
|
|
4708
|
+
fontSize: "11px",
|
|
4709
|
+
fontWeight: 600,
|
|
4710
|
+
color: actorColors[type] || actorColors.unknown
|
|
4711
|
+
}
|
|
4712
|
+
},
|
|
4713
|
+
username
|
|
4714
|
+
),
|
|
4715
|
+
h4(
|
|
4716
|
+
"span",
|
|
4717
|
+
{ style: { fontSize: "10px", color: "#4a4a5a" } },
|
|
4718
|
+
formatTime(msg.ts)
|
|
4719
|
+
)
|
|
4720
|
+
),
|
|
4721
|
+
h4(
|
|
4722
|
+
"div",
|
|
4723
|
+
{
|
|
4724
|
+
style: {
|
|
4725
|
+
background: isMe ? "#6366f1" : "#1e1e2e",
|
|
4726
|
+
color: isMe ? "#fff" : "#e2e2e8",
|
|
4727
|
+
padding: "6px 10px",
|
|
4728
|
+
borderRadius: "10px",
|
|
4729
|
+
borderTopRightRadius: isMe ? "2px" : "10px",
|
|
4730
|
+
borderTopLeftRadius: isMe ? "10px" : "2px",
|
|
4731
|
+
fontSize: "13px",
|
|
4732
|
+
lineHeight: 1.4,
|
|
4733
|
+
maxWidth: "240px",
|
|
4734
|
+
wordBreak: "break-word"
|
|
4735
|
+
}
|
|
4736
|
+
},
|
|
4737
|
+
msg.message
|
|
4738
|
+
)
|
|
4739
|
+
);
|
|
4740
|
+
})
|
|
4741
|
+
);
|
|
4742
|
+
const typingIndicator = typingUsers.length > 0 ? h4(
|
|
4743
|
+
"div",
|
|
4744
|
+
{
|
|
4745
|
+
style: {
|
|
4746
|
+
padding: "4px 12px",
|
|
4747
|
+
fontSize: "11px",
|
|
4748
|
+
color: "#6b6b80",
|
|
4749
|
+
fontStyle: "italic"
|
|
4750
|
+
}
|
|
4751
|
+
},
|
|
4752
|
+
typingUsers.map((id) => parseActorId(id).username).join(", ") + (typingUsers.length === 1 ? " is typing..." : " are typing...")
|
|
4753
|
+
) : null;
|
|
4754
|
+
const inputArea = h4(
|
|
4755
|
+
"div",
|
|
4756
|
+
{
|
|
4757
|
+
style: {
|
|
4758
|
+
padding: "8px 12px",
|
|
4759
|
+
borderTop: "1px solid #1e1e24",
|
|
4760
|
+
display: "flex",
|
|
4761
|
+
gap: "8px",
|
|
4762
|
+
flexShrink: 0
|
|
4763
|
+
}
|
|
4764
|
+
},
|
|
4765
|
+
h4("input", {
|
|
4766
|
+
type: "text",
|
|
4767
|
+
value: inputValue,
|
|
4768
|
+
onChange: handleInputChange,
|
|
4769
|
+
onKeyDown: handleKeyDown,
|
|
4770
|
+
placeholder: "Type a message...",
|
|
4771
|
+
disabled: sending,
|
|
4772
|
+
style: {
|
|
4773
|
+
flex: 1,
|
|
4774
|
+
padding: "6px 10px",
|
|
4775
|
+
fontSize: "13px",
|
|
4776
|
+
border: "1px solid #334155",
|
|
4777
|
+
borderRadius: "6px",
|
|
4778
|
+
background: "#1e293b",
|
|
4779
|
+
color: "#fff",
|
|
4780
|
+
outline: "none",
|
|
4781
|
+
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
4782
|
+
}
|
|
4783
|
+
}),
|
|
4784
|
+
h4(
|
|
4785
|
+
"button",
|
|
4786
|
+
{
|
|
4787
|
+
onClick: handleSend,
|
|
4788
|
+
disabled: sending || !inputValue.trim(),
|
|
4789
|
+
style: {
|
|
4790
|
+
padding: "6px 12px",
|
|
4791
|
+
borderRadius: "6px",
|
|
4792
|
+
background: "#6366f1",
|
|
4793
|
+
color: "#fff",
|
|
4794
|
+
border: "none",
|
|
4795
|
+
fontSize: "13px",
|
|
4796
|
+
cursor: sending || !inputValue.trim() ? "not-allowed" : "pointer",
|
|
4797
|
+
opacity: sending || !inputValue.trim() ? 0.5 : 1,
|
|
4798
|
+
fontWeight: 500
|
|
4799
|
+
}
|
|
4800
|
+
},
|
|
4801
|
+
"Send"
|
|
4802
|
+
)
|
|
4803
|
+
);
|
|
4804
|
+
if (embedded) {
|
|
4805
|
+
return h4(
|
|
4806
|
+
"div",
|
|
4807
|
+
{
|
|
4808
|
+
style: {
|
|
4809
|
+
display: "flex",
|
|
4810
|
+
flexDirection: "column",
|
|
4811
|
+
height: "100%",
|
|
4812
|
+
background: "#111113",
|
|
4813
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
4814
|
+
...style
|
|
4815
|
+
}
|
|
4816
|
+
},
|
|
4817
|
+
messageList,
|
|
4818
|
+
typingIndicator,
|
|
4819
|
+
inputArea
|
|
4820
|
+
);
|
|
4821
|
+
}
|
|
4822
|
+
const toggleBtn = h4(
|
|
4823
|
+
"button",
|
|
4824
|
+
{
|
|
4825
|
+
onClick: () => setOpen(!open),
|
|
4826
|
+
title: "Chat",
|
|
4827
|
+
style: {
|
|
4828
|
+
position: "fixed",
|
|
4829
|
+
bottom: "64px",
|
|
4830
|
+
right: "16px",
|
|
4831
|
+
zIndex: 9990,
|
|
4832
|
+
width: "40px",
|
|
4833
|
+
height: "40px",
|
|
4834
|
+
borderRadius: "50%",
|
|
4835
|
+
background: "#1e1e2e",
|
|
4836
|
+
border: "1px solid #334155",
|
|
4837
|
+
color: "#94a3b8",
|
|
4838
|
+
fontSize: "18px",
|
|
4839
|
+
cursor: "pointer",
|
|
4840
|
+
display: "flex",
|
|
4841
|
+
alignItems: "center",
|
|
4842
|
+
justifyContent: "center",
|
|
4843
|
+
transition: "background 0.15s",
|
|
4844
|
+
...style
|
|
4845
|
+
}
|
|
4846
|
+
},
|
|
4847
|
+
h4(
|
|
4848
|
+
"svg",
|
|
4849
|
+
{
|
|
4850
|
+
width: 20,
|
|
4851
|
+
height: 20,
|
|
4852
|
+
viewBox: "0 0 24 24",
|
|
4853
|
+
fill: "none",
|
|
4854
|
+
stroke: "currentColor",
|
|
4855
|
+
strokeWidth: 2,
|
|
4856
|
+
strokeLinecap: "round",
|
|
4857
|
+
strokeLinejoin: "round"
|
|
4858
|
+
},
|
|
4859
|
+
h4("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
4860
|
+
),
|
|
4861
|
+
unread > 0 ? h4(
|
|
4862
|
+
"span",
|
|
4863
|
+
{
|
|
4864
|
+
style: {
|
|
4865
|
+
position: "absolute",
|
|
4866
|
+
top: "-4px",
|
|
4867
|
+
right: "-4px",
|
|
4868
|
+
width: "18px",
|
|
4869
|
+
height: "18px",
|
|
4870
|
+
borderRadius: "50%",
|
|
4871
|
+
background: "#6366f1",
|
|
4872
|
+
color: "#fff",
|
|
4873
|
+
fontSize: "10px",
|
|
4874
|
+
fontWeight: 700,
|
|
4875
|
+
display: "flex",
|
|
4876
|
+
alignItems: "center",
|
|
4877
|
+
justifyContent: "center"
|
|
4878
|
+
}
|
|
4879
|
+
},
|
|
4880
|
+
unread > 9 ? "9+" : String(unread)
|
|
4881
|
+
) : null
|
|
4882
|
+
);
|
|
4883
|
+
if (!open) return toggleBtn;
|
|
4884
|
+
return h4(
|
|
4885
|
+
"div",
|
|
4886
|
+
null,
|
|
4887
|
+
toggleBtn,
|
|
4888
|
+
h4(
|
|
4889
|
+
"div",
|
|
4890
|
+
{
|
|
4891
|
+
style: {
|
|
4892
|
+
position: "fixed",
|
|
4893
|
+
bottom: "112px",
|
|
4894
|
+
right: "16px",
|
|
4895
|
+
zIndex: 9990,
|
|
4896
|
+
width: "320px",
|
|
4897
|
+
maxHeight: "500px",
|
|
4898
|
+
borderRadius: "12px",
|
|
4899
|
+
background: "#111113",
|
|
4900
|
+
border: "1px solid #1e1e24",
|
|
4901
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
|
|
4902
|
+
display: "flex",
|
|
4903
|
+
flexDirection: "column",
|
|
4904
|
+
overflow: "hidden",
|
|
4905
|
+
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
4906
|
+
}
|
|
4907
|
+
},
|
|
4908
|
+
// Header
|
|
4909
|
+
h4(
|
|
4910
|
+
"div",
|
|
4911
|
+
{
|
|
4912
|
+
style: {
|
|
4913
|
+
padding: "12px 16px",
|
|
4914
|
+
borderBottom: "1px solid #1e1e24",
|
|
4915
|
+
display: "flex",
|
|
4916
|
+
justifyContent: "space-between",
|
|
4917
|
+
alignItems: "center"
|
|
4918
|
+
}
|
|
4919
|
+
},
|
|
4920
|
+
h4(
|
|
4921
|
+
"span",
|
|
4922
|
+
{
|
|
4923
|
+
style: {
|
|
4924
|
+
fontSize: "12px",
|
|
4925
|
+
fontWeight: 700,
|
|
4926
|
+
color: "#6b6b80",
|
|
4927
|
+
textTransform: "uppercase",
|
|
4928
|
+
letterSpacing: "0.06em"
|
|
4929
|
+
}
|
|
4930
|
+
},
|
|
4931
|
+
"Chat"
|
|
4932
|
+
),
|
|
4933
|
+
h4(
|
|
4934
|
+
"button",
|
|
4935
|
+
{
|
|
4936
|
+
onClick: () => setOpen(false),
|
|
4937
|
+
style: {
|
|
4938
|
+
background: "none",
|
|
4939
|
+
border: "none",
|
|
4940
|
+
color: "#6b6b80",
|
|
4941
|
+
cursor: "pointer",
|
|
4942
|
+
fontSize: "16px",
|
|
4943
|
+
padding: "2px"
|
|
4944
|
+
}
|
|
4945
|
+
},
|
|
4946
|
+
"\u2715"
|
|
4947
|
+
)
|
|
4948
|
+
),
|
|
4949
|
+
messageList,
|
|
4950
|
+
typingIndicator,
|
|
4951
|
+
inputArea
|
|
4952
|
+
)
|
|
4953
|
+
);
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
// src/bug-report.ts
|
|
4957
|
+
function uid5() {
|
|
4958
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
4959
|
+
}
|
|
4960
|
+
function getReact8() {
|
|
4961
|
+
const R = globalThis.React;
|
|
4962
|
+
if (!R) throw new Error("React is not available.");
|
|
4963
|
+
return R;
|
|
4964
|
+
}
|
|
4965
|
+
function h5(type, props, ...children) {
|
|
4966
|
+
return getReact8().createElement(type, props, ...children);
|
|
4967
|
+
}
|
|
4968
|
+
function createBugReportTools(z) {
|
|
4969
|
+
return [
|
|
4970
|
+
{
|
|
4971
|
+
name: "_bug.report",
|
|
4972
|
+
description: "Submit a bug report with optional screenshot and description",
|
|
4973
|
+
input_schema: z.object({
|
|
4974
|
+
description: z.string().max(2e3).optional(),
|
|
4975
|
+
screenshot: z.string().optional().describe("Base64 PNG data URL of the current canvas state"),
|
|
4976
|
+
metadata: z.record(z.any()).optional().describe("Additional context (browser info, state snapshot, etc.)")
|
|
4977
|
+
}),
|
|
4978
|
+
risk: "low",
|
|
4979
|
+
capabilities_required: ["state.write"],
|
|
4980
|
+
handler: async (ctx, input) => {
|
|
4981
|
+
const report = {
|
|
4982
|
+
id: uid5(),
|
|
4983
|
+
actorId: ctx.actorId,
|
|
4984
|
+
description: input.description || "",
|
|
4985
|
+
screenshot: input.screenshot,
|
|
4986
|
+
metadata: input.metadata,
|
|
4987
|
+
ts: ctx.timestamp,
|
|
4988
|
+
status: "open"
|
|
4989
|
+
};
|
|
4990
|
+
const reports = [...ctx.state._bugReports || [], report].slice(-50);
|
|
4991
|
+
ctx.setState({ ...ctx.state, _bugReports: reports });
|
|
4992
|
+
return { reportId: report.id, totalReports: reports.length };
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
];
|
|
4996
|
+
}
|
|
4997
|
+
async function captureScreenshot() {
|
|
4998
|
+
try {
|
|
4999
|
+
const root = document.getElementById("root");
|
|
5000
|
+
if (!root) return null;
|
|
5001
|
+
const canvases = root.querySelectorAll("canvas");
|
|
5002
|
+
if (canvases.length === 1) {
|
|
5003
|
+
const cvs = canvases[0];
|
|
5004
|
+
const rootRect = root.getBoundingClientRect();
|
|
5005
|
+
const cvsRect = cvs.getBoundingClientRect();
|
|
5006
|
+
const fillsRoot = Math.abs(cvsRect.width - rootRect.width) < 20 && Math.abs(cvsRect.height - rootRect.height) < 20;
|
|
5007
|
+
if (fillsRoot) {
|
|
5008
|
+
return cvs.toDataURL("image/png");
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
const canvasSnapshots = /* @__PURE__ */ new Map();
|
|
5012
|
+
canvases.forEach((cvs) => {
|
|
5013
|
+
try {
|
|
5014
|
+
const dataUrl = cvs.toDataURL("image/png");
|
|
5015
|
+
if (dataUrl && dataUrl.length > 100) {
|
|
5016
|
+
canvasSnapshots.set(cvs, dataUrl);
|
|
5017
|
+
}
|
|
5018
|
+
} catch {
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
const html2canvas = globalThis.html2canvas;
|
|
5022
|
+
if (typeof html2canvas === "function") {
|
|
5023
|
+
const captured = await html2canvas(root, {
|
|
5024
|
+
backgroundColor: "#0a0a0a",
|
|
5025
|
+
useCORS: true,
|
|
5026
|
+
logging: false,
|
|
5027
|
+
scale: 0.5,
|
|
5028
|
+
onclone: (_doc, clonedRoot) => {
|
|
5029
|
+
const clonedCanvases = clonedRoot.querySelectorAll("canvas");
|
|
5030
|
+
clonedCanvases.forEach((clonedCvs, idx) => {
|
|
5031
|
+
const originalCvs = canvases[idx];
|
|
5032
|
+
const snapshot = canvasSnapshots.get(originalCvs);
|
|
5033
|
+
if (snapshot) {
|
|
5034
|
+
const img = _doc.createElement("img");
|
|
5035
|
+
img.src = snapshot;
|
|
5036
|
+
img.style.width = clonedCvs.style.width || `${clonedCvs.width}px`;
|
|
5037
|
+
img.style.height = clonedCvs.style.height || `${clonedCvs.height}px`;
|
|
5038
|
+
img.style.display = clonedCvs.style.display;
|
|
5039
|
+
clonedCvs.parentNode?.replaceChild(img, clonedCvs);
|
|
5040
|
+
}
|
|
5041
|
+
});
|
|
5042
|
+
}
|
|
5043
|
+
});
|
|
5044
|
+
return captured.toDataURL("image/png");
|
|
5045
|
+
}
|
|
5046
|
+
return null;
|
|
5047
|
+
} catch {
|
|
5048
|
+
return null;
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
function ReportBug({ callTool, actorId, style }) {
|
|
5052
|
+
const React4 = getReact8();
|
|
5053
|
+
const [phase, setPhase] = React4.useState("idle");
|
|
5054
|
+
const [screenshot, setScreenshot] = React4.useState(null);
|
|
5055
|
+
const [description, setDescription] = React4.useState("");
|
|
5056
|
+
const [submitting, setSubmitting] = React4.useState(false);
|
|
5057
|
+
const handleOpen = React4.useCallback(async () => {
|
|
5058
|
+
setPhase("capturing");
|
|
5059
|
+
const dataUrl = await captureScreenshot();
|
|
5060
|
+
setScreenshot(dataUrl);
|
|
5061
|
+
setPhase("form");
|
|
5062
|
+
}, []);
|
|
5063
|
+
const handleCancel = React4.useCallback(() => {
|
|
5064
|
+
setPhase("idle");
|
|
5065
|
+
setScreenshot(null);
|
|
5066
|
+
setDescription("");
|
|
5067
|
+
}, []);
|
|
5068
|
+
const handleSubmit = React4.useCallback(async () => {
|
|
5069
|
+
if (submitting) return;
|
|
5070
|
+
setSubmitting(true);
|
|
5071
|
+
try {
|
|
5072
|
+
await callTool("_bug.report", {
|
|
5073
|
+
description: description || void 0,
|
|
5074
|
+
screenshot: screenshot || void 0,
|
|
5075
|
+
metadata: { userAgent: navigator.userAgent }
|
|
5076
|
+
});
|
|
5077
|
+
setPhase("submitted");
|
|
5078
|
+
setDescription("");
|
|
5079
|
+
setScreenshot(null);
|
|
5080
|
+
setTimeout(() => setPhase("idle"), 2e3);
|
|
5081
|
+
} catch {
|
|
5082
|
+
} finally {
|
|
5083
|
+
setSubmitting(false);
|
|
5084
|
+
}
|
|
5085
|
+
}, [callTool, description, screenshot, submitting]);
|
|
5086
|
+
const bugButton = h5(
|
|
5087
|
+
"button",
|
|
5088
|
+
{
|
|
5089
|
+
onClick: phase === "idle" ? handleOpen : void 0,
|
|
5090
|
+
title: "Report Bug",
|
|
5091
|
+
style: {
|
|
5092
|
+
position: "fixed",
|
|
5093
|
+
bottom: "112px",
|
|
5094
|
+
right: "16px",
|
|
5095
|
+
zIndex: 9990,
|
|
5096
|
+
width: "40px",
|
|
5097
|
+
height: "40px",
|
|
5098
|
+
borderRadius: "50%",
|
|
5099
|
+
background: phase === "capturing" ? "#334155" : "#1e1e2e",
|
|
5100
|
+
border: "1px solid #334155",
|
|
5101
|
+
color: "#94a3b8",
|
|
5102
|
+
fontSize: "18px",
|
|
5103
|
+
cursor: phase === "idle" ? "pointer" : "default",
|
|
5104
|
+
display: "flex",
|
|
5105
|
+
alignItems: "center",
|
|
5106
|
+
justifyContent: "center",
|
|
5107
|
+
transition: "background 0.15s",
|
|
5108
|
+
...style
|
|
5109
|
+
}
|
|
5110
|
+
},
|
|
5111
|
+
phase === "capturing" ? (
|
|
5112
|
+
// Spinner
|
|
5113
|
+
h5(
|
|
5114
|
+
"svg",
|
|
5115
|
+
{
|
|
5116
|
+
width: 18,
|
|
5117
|
+
height: 18,
|
|
5118
|
+
viewBox: "0 0 24 24",
|
|
5119
|
+
fill: "none",
|
|
5120
|
+
stroke: "currentColor",
|
|
5121
|
+
strokeWidth: 2,
|
|
5122
|
+
style: { animation: "spin 0.8s linear infinite" }
|
|
5123
|
+
},
|
|
5124
|
+
h5("circle", { cx: 12, cy: 12, r: 10, strokeDasharray: "32", strokeDashoffset: "12" })
|
|
5125
|
+
)
|
|
5126
|
+
) : phase === "submitted" ? (
|
|
5127
|
+
// Checkmark
|
|
5128
|
+
h5(
|
|
5129
|
+
"svg",
|
|
5130
|
+
{
|
|
5131
|
+
width: 18,
|
|
5132
|
+
height: 18,
|
|
5133
|
+
viewBox: "0 0 24 24",
|
|
5134
|
+
fill: "none",
|
|
5135
|
+
stroke: "#22c55e",
|
|
5136
|
+
strokeWidth: 2.5,
|
|
5137
|
+
strokeLinecap: "round",
|
|
5138
|
+
strokeLinejoin: "round"
|
|
5139
|
+
},
|
|
5140
|
+
h5("polyline", { points: "20 6 9 17 4 12" })
|
|
5141
|
+
)
|
|
5142
|
+
) : (
|
|
5143
|
+
// Bug icon
|
|
5144
|
+
h5(
|
|
5145
|
+
"svg",
|
|
5146
|
+
{
|
|
5147
|
+
width: 18,
|
|
5148
|
+
height: 18,
|
|
5149
|
+
viewBox: "0 0 24 24",
|
|
5150
|
+
fill: "none",
|
|
5151
|
+
stroke: "currentColor",
|
|
5152
|
+
strokeWidth: 2,
|
|
5153
|
+
strokeLinecap: "round",
|
|
5154
|
+
strokeLinejoin: "round"
|
|
5155
|
+
},
|
|
5156
|
+
// Simple bug shape: body oval + antenna + legs
|
|
5157
|
+
h5("ellipse", { cx: 12, cy: 14, rx: 5, ry: 6 }),
|
|
5158
|
+
h5("path", { d: "M12 8V2" }),
|
|
5159
|
+
h5("path", { d: "M9 3l3 5 3-5" }),
|
|
5160
|
+
h5("path", { d: "M7 14H2" }),
|
|
5161
|
+
h5("path", { d: "M22 14h-5" }),
|
|
5162
|
+
h5("path", { d: "M7.5 10.5L4 8" }),
|
|
5163
|
+
h5("path", { d: "M16.5 10.5L20 8" })
|
|
5164
|
+
)
|
|
5165
|
+
)
|
|
5166
|
+
);
|
|
5167
|
+
if (phase === "idle" || phase === "capturing" || phase === "submitted") {
|
|
5168
|
+
return bugButton;
|
|
5169
|
+
}
|
|
5170
|
+
return h5(
|
|
5171
|
+
"div",
|
|
5172
|
+
null,
|
|
5173
|
+
bugButton,
|
|
5174
|
+
// Backdrop
|
|
5175
|
+
h5(
|
|
5176
|
+
"div",
|
|
5177
|
+
{
|
|
5178
|
+
onClick: handleCancel,
|
|
5179
|
+
style: {
|
|
5180
|
+
position: "fixed",
|
|
5181
|
+
inset: 0,
|
|
5182
|
+
zIndex: 9995,
|
|
5183
|
+
display: "flex",
|
|
5184
|
+
alignItems: "center",
|
|
5185
|
+
justifyContent: "center",
|
|
5186
|
+
background: "rgba(0,0,0,0.6)",
|
|
5187
|
+
backdropFilter: "blur(4px)"
|
|
5188
|
+
}
|
|
5189
|
+
},
|
|
5190
|
+
// Form panel
|
|
5191
|
+
h5(
|
|
5192
|
+
"div",
|
|
5193
|
+
{
|
|
5194
|
+
onClick: (e) => e.stopPropagation(),
|
|
5195
|
+
style: {
|
|
5196
|
+
background: "#1e1e2e",
|
|
5197
|
+
borderRadius: "12px",
|
|
5198
|
+
padding: "20px",
|
|
5199
|
+
boxShadow: "0 20px 60px rgba(0,0,0,0.5)",
|
|
5200
|
+
maxWidth: "400px",
|
|
5201
|
+
width: "90%",
|
|
5202
|
+
border: "1px solid #334155",
|
|
5203
|
+
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
5204
|
+
}
|
|
5205
|
+
},
|
|
5206
|
+
// Title
|
|
5207
|
+
h5(
|
|
5208
|
+
"div",
|
|
5209
|
+
{
|
|
5210
|
+
style: {
|
|
5211
|
+
display: "flex",
|
|
5212
|
+
justifyContent: "space-between",
|
|
5213
|
+
alignItems: "center",
|
|
5214
|
+
marginBottom: "16px"
|
|
5215
|
+
}
|
|
5216
|
+
},
|
|
5217
|
+
h5(
|
|
5218
|
+
"h3",
|
|
5219
|
+
{ style: { margin: 0, fontSize: "1.1rem", fontWeight: 600, color: "#fff" } },
|
|
5220
|
+
"Report Bug"
|
|
5221
|
+
),
|
|
5222
|
+
h5(
|
|
5223
|
+
"button",
|
|
5224
|
+
{
|
|
5225
|
+
onClick: handleCancel,
|
|
5226
|
+
style: {
|
|
5227
|
+
background: "none",
|
|
5228
|
+
border: "none",
|
|
5229
|
+
fontSize: "1.25rem",
|
|
5230
|
+
cursor: "pointer",
|
|
5231
|
+
color: "#94a3b8",
|
|
5232
|
+
padding: "4px"
|
|
5233
|
+
}
|
|
5234
|
+
},
|
|
5235
|
+
"\u2715"
|
|
5236
|
+
)
|
|
5237
|
+
),
|
|
5238
|
+
// Screenshot preview
|
|
5239
|
+
screenshot ? h5("img", {
|
|
5240
|
+
src: screenshot,
|
|
5241
|
+
alt: "Screenshot",
|
|
5242
|
+
style: {
|
|
5243
|
+
width: "100%",
|
|
5244
|
+
borderRadius: "8px",
|
|
5245
|
+
marginBottom: "12px",
|
|
5246
|
+
border: "1px solid #334155"
|
|
5247
|
+
}
|
|
5248
|
+
}) : h5(
|
|
5249
|
+
"div",
|
|
5250
|
+
{
|
|
5251
|
+
style: {
|
|
5252
|
+
padding: "24px",
|
|
5253
|
+
textAlign: "center",
|
|
5254
|
+
color: "#6b6b80",
|
|
5255
|
+
fontSize: "13px",
|
|
5256
|
+
border: "1px dashed #334155",
|
|
5257
|
+
borderRadius: "8px",
|
|
5258
|
+
marginBottom: "12px"
|
|
5259
|
+
}
|
|
5260
|
+
},
|
|
5261
|
+
"Screenshot unavailable"
|
|
5262
|
+
),
|
|
5263
|
+
// Description input
|
|
5264
|
+
h5("textarea", {
|
|
5265
|
+
value: description,
|
|
5266
|
+
onChange: (e) => setDescription(e.target.value),
|
|
5267
|
+
placeholder: "What's wrong? (optional)",
|
|
5268
|
+
rows: 3,
|
|
5269
|
+
style: {
|
|
5270
|
+
width: "100%",
|
|
5271
|
+
padding: "8px 12px",
|
|
5272
|
+
fontSize: "13px",
|
|
5273
|
+
border: "1px solid #334155",
|
|
5274
|
+
borderRadius: "6px",
|
|
5275
|
+
background: "#1e293b",
|
|
5276
|
+
color: "#fff",
|
|
5277
|
+
outline: "none",
|
|
5278
|
+
resize: "vertical",
|
|
5279
|
+
boxSizing: "border-box",
|
|
5280
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
5281
|
+
marginBottom: "12px"
|
|
5282
|
+
}
|
|
5283
|
+
}),
|
|
5284
|
+
// Action buttons
|
|
5285
|
+
h5(
|
|
5286
|
+
"div",
|
|
5287
|
+
{
|
|
5288
|
+
style: { display: "flex", gap: "8px", justifyContent: "flex-end" }
|
|
5289
|
+
},
|
|
5290
|
+
h5(
|
|
5291
|
+
"button",
|
|
5292
|
+
{
|
|
5293
|
+
onClick: handleCancel,
|
|
5294
|
+
style: {
|
|
5295
|
+
padding: "8px 16px",
|
|
5296
|
+
borderRadius: "6px",
|
|
5297
|
+
background: "transparent",
|
|
5298
|
+
color: "#94a3b8",
|
|
5299
|
+
border: "1px solid #334155",
|
|
5300
|
+
fontSize: "13px",
|
|
5301
|
+
cursor: "pointer",
|
|
5302
|
+
fontWeight: 500
|
|
5303
|
+
}
|
|
5304
|
+
},
|
|
5305
|
+
"Cancel"
|
|
5306
|
+
),
|
|
5307
|
+
h5(
|
|
5308
|
+
"button",
|
|
5309
|
+
{
|
|
5310
|
+
onClick: handleSubmit,
|
|
5311
|
+
disabled: submitting,
|
|
5312
|
+
style: {
|
|
5313
|
+
padding: "8px 16px",
|
|
5314
|
+
borderRadius: "6px",
|
|
5315
|
+
background: "#ef4444",
|
|
5316
|
+
color: "#fff",
|
|
5317
|
+
border: "none",
|
|
5318
|
+
fontSize: "13px",
|
|
5319
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
5320
|
+
opacity: submitting ? 0.5 : 1,
|
|
5321
|
+
fontWeight: 500
|
|
5322
|
+
}
|
|
5323
|
+
},
|
|
5324
|
+
submitting ? "Sending..." : "Submit Report"
|
|
5325
|
+
)
|
|
5326
|
+
)
|
|
5327
|
+
)
|
|
5328
|
+
)
|
|
5329
|
+
);
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
// src/composability.ts
|
|
5333
|
+
function importTools(experienceModule, toolNames, prefix) {
|
|
5334
|
+
if (!experienceModule?.tools) {
|
|
5335
|
+
throw new Error("importTools: experience module has no tools array");
|
|
5336
|
+
}
|
|
5337
|
+
let tools;
|
|
5338
|
+
if (toolNames === "*") {
|
|
5339
|
+
tools = [...experienceModule.tools];
|
|
5340
|
+
} else {
|
|
5341
|
+
tools = experienceModule.tools.filter((t) => toolNames.includes(t.name));
|
|
5342
|
+
const found = tools.map((t) => t.name);
|
|
5343
|
+
const missing = toolNames.filter((n) => !found.includes(n));
|
|
5344
|
+
if (missing.length > 0) {
|
|
5345
|
+
throw new Error(`importTools: tools not found: ${missing.join(", ")}`);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
if (prefix) {
|
|
5349
|
+
return tools.map((t) => ({
|
|
5350
|
+
...t,
|
|
5351
|
+
name: `${prefix}.${t.name}`
|
|
5352
|
+
}));
|
|
5353
|
+
}
|
|
5354
|
+
return tools;
|
|
5355
|
+
}
|
|
5356
|
+
function getReact9() {
|
|
5357
|
+
const R = globalThis.React;
|
|
5358
|
+
if (!R) throw new Error("React is not available.");
|
|
5359
|
+
return R;
|
|
5360
|
+
}
|
|
5361
|
+
function h6(type, props, ...children) {
|
|
5362
|
+
return getReact9().createElement(type, props, ...children);
|
|
5363
|
+
}
|
|
5364
|
+
function EmbeddedExperience(props) {
|
|
5365
|
+
const React4 = getReact9();
|
|
5366
|
+
const {
|
|
5367
|
+
experience,
|
|
5368
|
+
stateKey,
|
|
5369
|
+
sharedState,
|
|
5370
|
+
callTool,
|
|
5371
|
+
roomId,
|
|
5372
|
+
actorId,
|
|
5373
|
+
participants,
|
|
5374
|
+
ephemeralState,
|
|
5375
|
+
setEphemeral,
|
|
5376
|
+
roomConfig,
|
|
5377
|
+
style,
|
|
5378
|
+
className
|
|
5379
|
+
} = props;
|
|
5380
|
+
const childState = React4.useMemo(
|
|
5381
|
+
() => sharedState[stateKey] || {},
|
|
5382
|
+
[sharedState, stateKey]
|
|
5383
|
+
);
|
|
5384
|
+
const scopedCallTool = React4.useCallback(
|
|
5385
|
+
async (name, input) => {
|
|
5386
|
+
return callTool(`${stateKey}:${name}`, { ...input, _scopeKey: stateKey });
|
|
5387
|
+
},
|
|
5388
|
+
[callTool, stateKey]
|
|
5389
|
+
);
|
|
5390
|
+
const childEphemeral = React4.useMemo(() => {
|
|
5391
|
+
const scoped = {};
|
|
5392
|
+
for (const [actId, data] of Object.entries(ephemeralState)) {
|
|
5393
|
+
if (data[stateKey]) {
|
|
5394
|
+
scoped[actId] = data[stateKey];
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
return scoped;
|
|
5398
|
+
}, [ephemeralState, stateKey]);
|
|
5399
|
+
const scopedSetEphemeral = React4.useCallback(
|
|
5400
|
+
(data) => {
|
|
5401
|
+
setEphemeral({ [stateKey]: data });
|
|
5402
|
+
},
|
|
5403
|
+
[setEphemeral, stateKey]
|
|
5404
|
+
);
|
|
5405
|
+
const ChildCanvas = experience.Canvas;
|
|
5406
|
+
return h6(
|
|
5407
|
+
"div",
|
|
5408
|
+
{ style, className },
|
|
5409
|
+
h6(ChildCanvas, {
|
|
5410
|
+
roomId,
|
|
5411
|
+
actorId,
|
|
5412
|
+
sharedState: childState,
|
|
5413
|
+
callTool: scopedCallTool,
|
|
5414
|
+
ephemeralState: childEphemeral,
|
|
5415
|
+
setEphemeral: scopedSetEphemeral,
|
|
5416
|
+
participants,
|
|
5417
|
+
roomConfig: roomConfig || {}
|
|
5418
|
+
})
|
|
5419
|
+
);
|
|
5420
|
+
}
|
|
694
5421
|
export {
|
|
695
5422
|
Badge,
|
|
696
5423
|
Button,
|
|
697
5424
|
Card,
|
|
5425
|
+
ChatPanel,
|
|
5426
|
+
ColorPicker,
|
|
5427
|
+
Dropdown,
|
|
5428
|
+
EmbeddedExperience,
|
|
698
5429
|
Grid,
|
|
699
5430
|
InMemoryAdapter,
|
|
700
5431
|
Input,
|
|
5432
|
+
Modal,
|
|
5433
|
+
PathBuilder,
|
|
5434
|
+
PixiSceneRenderer,
|
|
5435
|
+
ReportBug,
|
|
5436
|
+
SceneRenderer,
|
|
5437
|
+
Slider,
|
|
701
5438
|
Stack,
|
|
5439
|
+
SvgSceneRenderer,
|
|
5440
|
+
Tabs,
|
|
5441
|
+
Textarea,
|
|
5442
|
+
allNodeIds,
|
|
5443
|
+
cloneScene2 as cloneScene,
|
|
702
5444
|
compareSemver,
|
|
703
|
-
createAgentProtocolHints,
|
|
704
5445
|
createAgentProtocolTools,
|
|
5446
|
+
createBugReportTools,
|
|
5447
|
+
createChatTools,
|
|
5448
|
+
createNode,
|
|
5449
|
+
createRuleTools,
|
|
5450
|
+
createScene,
|
|
5451
|
+
createSceneSchemas,
|
|
5452
|
+
createSceneTools,
|
|
705
5453
|
defineEphemeralAction,
|
|
706
5454
|
defineExperience,
|
|
5455
|
+
defineRoomConfig,
|
|
5456
|
+
defineStream,
|
|
707
5457
|
defineTest,
|
|
708
5458
|
defineTool,
|
|
5459
|
+
easingFunctions,
|
|
5460
|
+
findNodes,
|
|
5461
|
+
findParent,
|
|
5462
|
+
getPath,
|
|
709
5463
|
getStateVersion,
|
|
5464
|
+
importTools,
|
|
5465
|
+
interpolateTween,
|
|
710
5466
|
migrateState,
|
|
5467
|
+
nodeById,
|
|
5468
|
+
nodeCount,
|
|
5469
|
+
nodeMatchesSelector,
|
|
5470
|
+
phaseTool,
|
|
711
5471
|
quickTool,
|
|
5472
|
+
removeNodeById,
|
|
5473
|
+
ruleTools,
|
|
5474
|
+
sceneTools,
|
|
5475
|
+
setPath,
|
|
5476
|
+
spawnParticles,
|
|
5477
|
+
tickParticleNode,
|
|
5478
|
+
tickParticles,
|
|
5479
|
+
undoTool,
|
|
712
5480
|
useAnimationFrame,
|
|
5481
|
+
useBlob,
|
|
5482
|
+
useChat,
|
|
5483
|
+
useDebounce,
|
|
713
5484
|
useFollow,
|
|
714
5485
|
useOptimisticTool,
|
|
715
5486
|
useParticipants,
|
|
5487
|
+
useParticleTick,
|
|
5488
|
+
usePhase,
|
|
5489
|
+
useRuleTick,
|
|
5490
|
+
useSceneDrag,
|
|
5491
|
+
useSceneInteraction,
|
|
5492
|
+
useSceneSelection,
|
|
5493
|
+
useSceneTweens,
|
|
5494
|
+
useSceneViewport,
|
|
716
5495
|
useSharedState,
|
|
5496
|
+
useThrottle,
|
|
717
5497
|
useToolCall,
|
|
718
5498
|
useTypingIndicator,
|
|
719
|
-
|
|
5499
|
+
useUndo,
|
|
5500
|
+
validateExperience,
|
|
5501
|
+
walkNodes
|
|
720
5502
|
};
|