@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/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
- validateExperience
5499
+ useUndo,
5500
+ validateExperience,
5501
+ walkNodes
720
5502
  };